portacode 0.3.20.dev4__tar.gz → 0.3.20.dev6__tar.gz

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 (80) hide show
  1. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/PKG-INFO +1 -1
  2. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/_version.py +2 -2
  3. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/centralized_handlers.py +96 -0
  4. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/centralized_state.py +19 -3
  5. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/git_manager.py +56 -4
  6. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/simplified_file_watcher.py +1 -1
  7. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state_handlers.py +12 -12
  8. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode.egg-info/PKG-INFO +1 -1
  9. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/.claude/agents/communication-manager.md +0 -0
  10. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/.claude/settings.local.json +0 -0
  11. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/.gitignore +0 -0
  12. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/.gitmodules +0 -0
  13. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/LICENSE +0 -0
  14. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/MANIFEST.in +0 -0
  15. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/Makefile +0 -0
  16. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/README.md +0 -0
  17. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/backup.sh +0 -0
  18. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/docker-compose.yaml +0 -0
  19. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/README.md +0 -0
  20. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/__init__.py +0 -0
  21. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/__main__.py +0 -0
  22. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/cli.py +0 -0
  23. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/README.md +0 -0
  24. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/__init__.py +0 -0
  25. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/client.py +0 -0
  26. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/README.md +0 -0
  27. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
  28. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/__init__.py +0 -0
  29. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/base.py +0 -0
  30. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/file_handlers.py +0 -0
  31. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/README.md +0 -0
  32. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/__init__.py +0 -0
  33. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/centralized_manager.py +0 -0
  34. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  35. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/handlers.py +0 -0
  36. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/manager.py +0 -0
  37. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/models.py +0 -0
  38. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/project_state/utils.py +0 -0
  39. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/registry.py +0 -0
  40. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/session.py +0 -0
  41. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/system_handlers.py +0 -0
  42. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/tab_factory.py +0 -0
  43. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/handlers/terminal_handlers.py +0 -0
  44. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/multiplex.py +0 -0
  45. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/connection/terminal.py +0 -0
  46. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/data.py +0 -0
  47. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/keypair.py +0 -0
  48. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode/service.py +0 -0
  49. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode.egg-info/SOURCES.txt +0 -0
  50. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode.egg-info/dependency_links.txt +0 -0
  51. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode.egg-info/entry_points.txt +0 -0
  52. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode.egg-info/requires.txt +0 -0
  53. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/portacode.egg-info/top_level.txt +0 -0
  54. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/pyproject.toml +0 -0
  55. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/restore.sh +0 -0
  56. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/run_tests.py +0 -0
  57. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/setup.cfg +0 -0
  58. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/setup.py +0 -0
  59. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test.sh +0 -0
  60. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/README.md +0 -0
  61. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/__init__.py +0 -0
  62. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/test_device_online.py +0 -0
  63. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/test_file_operations.py +0 -0
  64. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/test_login_flow.py +0 -0
  65. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/test_navigate_testing_folder.py +0 -0
  66. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/test_terminal_interaction.py +0 -0
  67. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/test_modules/test_terminal_start.py +0 -0
  68. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/.env.example +0 -0
  69. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/README.md +0 -0
  70. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/__init__.py +0 -0
  71. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/cli.py +0 -0
  72. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/__init__.py +0 -0
  73. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/base_test.py +0 -0
  74. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/cli_manager.py +0 -0
  75. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/hierarchical_runner.py +0 -0
  76. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/playwright_manager.py +0 -0
  77. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/runner.py +0 -0
  78. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/shared_cli_manager.py +0 -0
  79. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/core/test_discovery.py +0 -0
  80. {portacode-0.3.20.dev4 → portacode-0.3.20.dev6}/testing_framework/requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.20.dev4
3
+ Version: 0.3.20.dev6
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.20.dev4'
21
- __version_tuple__ = version_tuple = (0, 3, 20, 'dev4')
20
+ __version__ = version = '0.3.20.dev6'
21
+ __version_tuple__ = version_tuple = (0, 3, 20, 'dev6')
@@ -268,6 +268,102 @@ class CentralizedProjectStateGitRevertHandler(AsyncHandler):
268
268
  }
269
269
 
270
270
 
271
+ class CentralizedProjectStateSetActiveTabHandler(AsyncHandler):
272
+ """Handler for setting active tab using centralized state."""
273
+
274
+ @property
275
+ def command_name(self) -> str:
276
+ return "project_state_set_active_tab"
277
+
278
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
279
+ """Set active tab in project state."""
280
+ server_project_id = message.get("project_id")
281
+ tab_id = message.get("tab_id") # Can be None to clear active tab
282
+ source_client_session = message.get("source_client_session")
283
+
284
+ if not server_project_id:
285
+ raise ValueError("project_id is required")
286
+ if not source_client_session:
287
+ raise ValueError("source_client_session is required")
288
+
289
+ logger.info("Setting active tab %s for session %s", tab_id, source_client_session)
290
+
291
+ # Note: Active tab is deprecated in the new centralized system
292
+ # This handler maintains compatibility but does nothing
293
+ logger.info("Active tab functionality is deprecated in centralized system")
294
+
295
+ return {
296
+ "event": "project_state_set_active_tab_response",
297
+ "project_id": server_project_id,
298
+ "tab_id": tab_id,
299
+ "success": True
300
+ }
301
+
302
+
303
+ class CentralizedProjectStateDiffOpenHandler(AsyncHandler):
304
+ """Handler for opening diff tabs using centralized state."""
305
+
306
+ @property
307
+ def command_name(self) -> str:
308
+ return "project_state_diff_open"
309
+
310
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
311
+ """Open a diff tab comparing file versions at different git timeline points."""
312
+ server_project_id = message.get("project_id")
313
+ file_path = message.get("file_path")
314
+ from_ref = message.get("from_ref")
315
+ to_ref = message.get("to_ref")
316
+ from_hash = message.get("from_hash")
317
+ to_hash = message.get("to_hash")
318
+ source_client_session = message.get("source_client_session")
319
+
320
+ if not server_project_id:
321
+ raise ValueError("project_id is required")
322
+ if not file_path:
323
+ raise ValueError("file_path is required")
324
+ if not from_ref:
325
+ raise ValueError("from_ref is required")
326
+ if not to_ref:
327
+ raise ValueError("to_ref is required")
328
+ if not source_client_session:
329
+ raise ValueError("source_client_session is required")
330
+
331
+ # Validate reference types
332
+ valid_refs = {'head', 'staged', 'working', 'commit'}
333
+ if from_ref not in valid_refs:
334
+ raise ValueError(f"Invalid from_ref: {from_ref}. Must be one of {valid_refs}")
335
+ if to_ref not in valid_refs:
336
+ raise ValueError(f"Invalid to_ref: {to_ref}. Must be one of {valid_refs}")
337
+
338
+ # Validate commit hashes are provided when needed
339
+ if from_ref == 'commit' and not from_hash:
340
+ raise ValueError("from_hash is required when from_ref='commit'")
341
+ if to_ref == 'commit' and not to_hash:
342
+ raise ValueError("to_hash is required when to_ref='commit'")
343
+
344
+ logger.info("Opening diff tab %s (%s->%s) for session %s",
345
+ file_path, from_ref, to_ref, source_client_session)
346
+
347
+ # Get centralized manager
348
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
349
+
350
+ # For now, return success but don't actually implement diff tabs
351
+ # This would need to be implemented in the centralized manager
352
+ logger.info("Diff tab functionality not yet implemented in centralized system")
353
+
354
+ return {
355
+ "event": "project_state_diff_open_response",
356
+ "project_id": server_project_id,
357
+ "file_path": file_path,
358
+ "from_ref": from_ref,
359
+ "to_ref": to_ref,
360
+ "from_hash": from_hash,
361
+ "to_hash": to_hash,
362
+ "success": False, # Not implemented yet
363
+ "error": "Diff tab functionality not yet implemented in centralized system"
364
+ }
365
+
366
+
271
367
  # Handler for explicit client session cleanup
272
368
  async def handle_centralized_client_session_cleanup(handler, payload: Dict[str, Any],
273
369
  source_client_session: str) -> Dict[str, Any]:
@@ -78,6 +78,7 @@ class CentralizedProjectState:
78
78
 
79
79
  # Single source of truth for git state
80
80
  self._git_snapshot: Optional[GitStateSnapshot] = None
81
+ self._last_git_status_hash: Optional[str] = None
81
82
 
82
83
  # File system state
83
84
  self._monitored_folders: List[MonitoredFolder] = []
@@ -228,14 +229,15 @@ class StateUpdateManager:
228
229
  self.git_manager = git_manager
229
230
 
230
231
  async def refresh_git_state(self, state: CentralizedProjectState) -> bool:
231
- """Refresh git state from repository. Returns True if state changed."""
232
+ """Refresh git state from repository using hash-based change detection. Returns True if state changed."""
232
233
  try:
233
234
  # Check if .git directory exists
234
235
  git_dir = os.path.join(state.project_folder_path, '.git')
235
236
  is_git_repo = os.path.exists(git_dir)
236
237
 
237
238
  if not is_git_repo:
238
- # No git repository
239
+ # No git repository - reset hash and set empty state
240
+ state._last_git_status_hash = None
239
241
  empty_snapshot = GitStateSnapshot(
240
242
  is_git_repo=False,
241
243
  branch_name=None,
@@ -250,7 +252,19 @@ class StateUpdateManager:
250
252
  if not self.git_manager.is_git_repo:
251
253
  self.git_manager.reinitialize()
252
254
 
253
- # Get complete git state atomically
255
+ # Use hash-based change detection for performance
256
+ current_git_hash = self.git_manager.compute_git_status_hash()
257
+
258
+ # Only compute detailed status if hash changed
259
+ if current_git_hash == state._last_git_status_hash:
260
+ logger.debug("Git status hash unchanged for %s, skipping detailed computation",
261
+ state.client_session_id)
262
+ return False
263
+
264
+ logger.debug("Git status hash changed for %s: %s -> %s",
265
+ state.client_session_id, state._last_git_status_hash, current_git_hash)
266
+
267
+ # Hash changed, compute detailed status
254
268
  branch_name = self.git_manager.get_branch_name()
255
269
  detailed_status = self.git_manager.get_detailed_status()
256
270
 
@@ -263,6 +277,8 @@ class StateUpdateManager:
263
277
  untracked_files=detailed_status.untracked_files
264
278
  )
265
279
 
280
+ # Update hash and state
281
+ state._last_git_status_hash = current_git_hash
266
282
  return state.update_git_state(new_snapshot)
267
283
 
268
284
  except Exception as e:
@@ -851,6 +851,46 @@ class GitManager:
851
851
  logger.debug("Error getting HEAD commit hash: %s", e)
852
852
  return None
853
853
 
854
+ def compute_git_status_hash(self) -> str:
855
+ """Compute a hash representing the current git status.
856
+
857
+ This method uses GitPython to quickly get git status information
858
+ and computes a hash that changes when the status changes.
859
+ """
860
+ if not self.is_git_repo or not self.repo:
861
+ return "no-repo"
862
+
863
+ import hashlib
864
+
865
+ try:
866
+ status_components = []
867
+
868
+ # Include HEAD commit hash
869
+ head_hash = self.get_head_commit_hash() or "no-head"
870
+ status_components.append(f"head:{head_hash}")
871
+
872
+ # Include branch name
873
+ branch = self.get_branch_name() or "no-branch"
874
+ status_components.append(f"branch:{branch}")
875
+
876
+ # Use git status --porcelain for consistent status representation
877
+ porcelain_status = self.repo.git.status("--porcelain")
878
+ status_components.append(f"porcelain:{porcelain_status}")
879
+
880
+ # Include index file modification time as a fallback for edge cases
881
+ index_path = os.path.join(self.repo.git_dir, 'index')
882
+ if os.path.exists(index_path):
883
+ index_mtime = str(os.path.getmtime(index_path))
884
+ status_components.append(f"index_mtime:{index_mtime}")
885
+
886
+ # Combine all components and hash
887
+ combined_status = "|".join(status_components)
888
+ return hashlib.sha256(combined_status.encode('utf-8')).hexdigest()
889
+
890
+ except Exception as e:
891
+ logger.error("Error computing git status hash: %s", e)
892
+ return f"error-{str(hash(str(e)))}"
893
+
854
894
  def get_detailed_status(self) -> GitDetailedStatus:
855
895
  """Get detailed Git status with file hashes using GitPython APIs."""
856
896
  if not self.is_git_repo or not self.repo:
@@ -1092,13 +1132,25 @@ class GitManager:
1092
1132
 
1093
1133
  if has_commits:
1094
1134
  # Repository has commits - use git restore --staged
1095
- self.repo.git.restore('--staged', rel_path)
1096
- logger.info("Successfully unstaged file using restore: %s", rel_path)
1135
+ try:
1136
+ self.repo.git.restore('--staged', rel_path)
1137
+ logger.info("Successfully unstaged file using restore: %s", rel_path)
1138
+ except Exception as restore_error:
1139
+ # If restore fails, try reset HEAD approach
1140
+ logger.debug("git restore failed, trying reset HEAD: %s", restore_error)
1141
+ self.repo.git.reset('HEAD', rel_path)
1142
+ logger.info("Successfully unstaged file using reset HEAD: %s", rel_path)
1097
1143
  else:
1098
1144
  # Repository has no commits - use git rm --cached
1099
1145
  # This handles the case where files are staged but no initial commit exists
1100
- self.repo.git.rm('--cached', rel_path)
1101
- logger.info("Successfully unstaged file using rm --cached (no HEAD): %s", rel_path)
1146
+ try:
1147
+ self.repo.git.rm('--cached', rel_path)
1148
+ logger.info("Successfully unstaged file using rm --cached (no HEAD): %s", rel_path)
1149
+ except Exception as rm_error:
1150
+ # If rm --cached fails, try with --force flag
1151
+ logger.debug("git rm --cached failed, trying with --force: %s", rm_error)
1152
+ self.repo.git.rm('--cached', '--force', rel_path)
1153
+ logger.info("Successfully unstaged file using rm --cached --force (no HEAD): %s", rel_path)
1102
1154
 
1103
1155
  return True
1104
1156
 
@@ -110,7 +110,7 @@ class SimplifiedEventHandler(FileSystemEventHandler):
110
110
 
111
111
  # Skip git directories entirely - they're handled by periodic polling
112
112
  path_parts = Path(event.src_path).parts
113
- if '.git' in path_parts:
113
+ if '.git' in path_parts or event.src_path.endswith(('.git', '/.git')):
114
114
  logger.debug("Skipping git directory event: %s", event.src_path)
115
115
  return
116
116
 
@@ -15,18 +15,18 @@ project_state/README.md
15
15
  # Import everything from the modular structure for backward compatibility
16
16
  from .project_state import *
17
17
 
18
- # Ensure all handlers are available at module level for existing imports
19
- from .project_state.handlers import (
20
- ProjectStateFolderExpandHandler,
21
- ProjectStateFolderCollapseHandler,
22
- ProjectStateFileOpenHandler,
23
- ProjectStateTabCloseHandler,
24
- ProjectStateSetActiveTabHandler,
25
- ProjectStateDiffOpenHandler,
26
- ProjectStateGitStageHandler,
27
- ProjectStateGitUnstageHandler,
28
- ProjectStateGitRevertHandler,
29
- handle_client_session_cleanup
18
+ # Use the new centralized handlers for better performance and reliability
19
+ from .project_state.centralized_handlers import (
20
+ CentralizedProjectStateFolderExpandHandler as ProjectStateFolderExpandHandler,
21
+ CentralizedProjectStateFolderCollapseHandler as ProjectStateFolderCollapseHandler,
22
+ CentralizedProjectStateFileOpenHandler as ProjectStateFileOpenHandler,
23
+ CentralizedProjectStateTabCloseHandler as ProjectStateTabCloseHandler,
24
+ CentralizedProjectStateSetActiveTabHandler as ProjectStateSetActiveTabHandler,
25
+ CentralizedProjectStateDiffOpenHandler as ProjectStateDiffOpenHandler,
26
+ CentralizedProjectStateGitStageHandler as ProjectStateGitStageHandler,
27
+ CentralizedProjectStateGitUnstageHandler as ProjectStateGitUnstageHandler,
28
+ CentralizedProjectStateGitRevertHandler as ProjectStateGitRevertHandler,
29
+ handle_centralized_client_session_cleanup as handle_client_session_cleanup
30
30
  )
31
31
 
32
32
  from .project_state.manager import (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.20.dev4
3
+ Version: 0.3.20.dev6
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
File without changes
File without changes