portacode 0.3.20.dev3__tar.gz → 0.3.20.dev4__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 (81) hide show
  1. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/PKG-INFO +1 -1
  2. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/_version.py +2 -2
  3. portacode-0.3.20.dev4/portacode/connection/handlers/project_state/__init__.py +145 -0
  4. portacode-0.3.20.dev4/portacode/connection/handlers/project_state/centralized_handlers.py +299 -0
  5. portacode-0.3.20.dev4/portacode/connection/handlers/project_state/centralized_manager.py +410 -0
  6. portacode-0.3.20.dev4/portacode/connection/handlers/project_state/centralized_state.py +441 -0
  7. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state/git_manager.py +6 -1
  8. portacode-0.3.20.dev4/portacode/connection/handlers/project_state/simplified_file_watcher.py +138 -0
  9. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state/utils.py +27 -0
  10. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode.egg-info/PKG-INFO +1 -1
  11. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode.egg-info/SOURCES.txt +4 -0
  12. portacode-0.3.20.dev3/portacode/connection/handlers/project_state/__init__.py +0 -90
  13. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/.claude/agents/communication-manager.md +0 -0
  14. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/.claude/settings.local.json +0 -0
  15. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/.gitignore +0 -0
  16. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/.gitmodules +0 -0
  17. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/LICENSE +0 -0
  18. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/MANIFEST.in +0 -0
  19. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/Makefile +0 -0
  20. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/README.md +0 -0
  21. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/backup.sh +0 -0
  22. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/docker-compose.yaml +0 -0
  23. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/README.md +0 -0
  24. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/__init__.py +0 -0
  25. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/__main__.py +0 -0
  26. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/cli.py +0 -0
  27. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/README.md +0 -0
  28. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/__init__.py +0 -0
  29. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/client.py +0 -0
  30. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/README.md +0 -0
  31. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
  32. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/__init__.py +0 -0
  33. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/base.py +0 -0
  34. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/file_handlers.py +0 -0
  35. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state/README.md +0 -0
  36. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  37. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state/handlers.py +0 -0
  38. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state/manager.py +0 -0
  39. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state/models.py +0 -0
  40. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/project_state_handlers.py +0 -0
  41. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/registry.py +0 -0
  42. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/session.py +0 -0
  43. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/system_handlers.py +0 -0
  44. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/tab_factory.py +0 -0
  45. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/handlers/terminal_handlers.py +0 -0
  46. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/multiplex.py +0 -0
  47. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/connection/terminal.py +0 -0
  48. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/data.py +0 -0
  49. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/keypair.py +0 -0
  50. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode/service.py +0 -0
  51. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode.egg-info/dependency_links.txt +0 -0
  52. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode.egg-info/entry_points.txt +0 -0
  53. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode.egg-info/requires.txt +0 -0
  54. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/portacode.egg-info/top_level.txt +0 -0
  55. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/pyproject.toml +0 -0
  56. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/restore.sh +0 -0
  57. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/run_tests.py +0 -0
  58. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/setup.cfg +0 -0
  59. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/setup.py +0 -0
  60. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test.sh +0 -0
  61. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/README.md +0 -0
  62. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/__init__.py +0 -0
  63. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/test_device_online.py +0 -0
  64. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/test_file_operations.py +0 -0
  65. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/test_login_flow.py +0 -0
  66. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/test_navigate_testing_folder.py +0 -0
  67. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/test_terminal_interaction.py +0 -0
  68. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/test_modules/test_terminal_start.py +0 -0
  69. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/.env.example +0 -0
  70. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/README.md +0 -0
  71. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/__init__.py +0 -0
  72. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/cli.py +0 -0
  73. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/__init__.py +0 -0
  74. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/base_test.py +0 -0
  75. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/cli_manager.py +0 -0
  76. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/hierarchical_runner.py +0 -0
  77. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/playwright_manager.py +0 -0
  78. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/runner.py +0 -0
  79. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/shared_cli_manager.py +0 -0
  80. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/testing_framework/core/test_discovery.py +0 -0
  81. {portacode-0.3.20.dev3 → portacode-0.3.20.dev4}/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.dev3
3
+ Version: 0.3.20.dev4
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.dev3'
21
- __version_tuple__ = version_tuple = (0, 3, 20, 'dev3')
20
+ __version__ = version = '0.3.20.dev4'
21
+ __version_tuple__ = version_tuple = (0, 3, 20, 'dev4')
@@ -0,0 +1,145 @@
1
+ """Project State Management Package
2
+
3
+ This package provides a modular architecture for managing project state in the
4
+ Portacode application, including file system monitoring, git integration,
5
+ tab management, and real-time state synchronization.
6
+
7
+ The package is organized into the following modules:
8
+
9
+ - models: Data structures and models (ProjectState, FileItem, TabInfo, etc.)
10
+ - git_manager: Git operations and repository management
11
+ - file_system_watcher: File system change monitoring
12
+ - manager: Central project state coordinator (legacy)
13
+ - centralized_manager: New centralized state manager with single source of truth
14
+ - handlers: Request handlers for various operations (legacy)
15
+ - centralized_handlers: New handlers using centralized state management
16
+ - utils: Utility functions and helpers
17
+
18
+ Usage (New Centralized System):
19
+ from project_state.centralized_manager import get_or_create_centralized_manager
20
+ from project_state.centralized_handlers import CentralizedProjectStateFolderExpandHandler
21
+ from project_state.centralized_state import CentralizedProjectState
22
+
23
+ Usage (Legacy System):
24
+ from project_state.manager import get_or_create_project_state_manager
25
+ from project_state.handlers import ProjectStateFolderExpandHandler
26
+ from project_state.models import ProjectState, FileItem
27
+ """
28
+
29
+ # Public API exports
30
+ from .models import (
31
+ ProjectState,
32
+ FileItem,
33
+ TabInfo,
34
+ MonitoredFolder,
35
+ GitFileChange,
36
+ GitDetailedStatus
37
+ )
38
+
39
+ # Legacy manager (for backwards compatibility)
40
+ from .manager import (
41
+ ProjectStateManager,
42
+ get_or_create_project_state_manager,
43
+ reset_global_project_state_manager,
44
+ debug_global_manager_state
45
+ )
46
+
47
+ # New centralized system
48
+ from .centralized_manager import (
49
+ CentralizedProjectStateManager,
50
+ get_or_create_centralized_manager
51
+ )
52
+
53
+ from .centralized_state import (
54
+ CentralizedProjectState,
55
+ GitStateSnapshot,
56
+ StateUpdateManager,
57
+ StateNotificationManager,
58
+ PeriodicGitMonitor
59
+ )
60
+
61
+ from .git_manager import GitManager
62
+ from .file_system_watcher import FileSystemWatcher
63
+
64
+ # Legacy handlers (for backwards compatibility)
65
+ from .handlers import (
66
+ ProjectStateFolderExpandHandler,
67
+ ProjectStateFolderCollapseHandler,
68
+ ProjectStateFileOpenHandler,
69
+ ProjectStateTabCloseHandler,
70
+ ProjectStateSetActiveTabHandler,
71
+ ProjectStateDiffOpenHandler,
72
+ ProjectStateGitStageHandler,
73
+ ProjectStateGitUnstageHandler,
74
+ ProjectStateGitRevertHandler,
75
+ handle_client_session_cleanup
76
+ )
77
+
78
+ # New centralized handlers
79
+ from .centralized_handlers import (
80
+ CentralizedProjectStateFolderExpandHandler,
81
+ CentralizedProjectStateFolderCollapseHandler,
82
+ CentralizedProjectStateFileOpenHandler,
83
+ CentralizedProjectStateTabCloseHandler,
84
+ CentralizedProjectStateGitStageHandler,
85
+ CentralizedProjectStateGitUnstageHandler,
86
+ CentralizedProjectStateGitRevertHandler,
87
+ handle_centralized_client_session_cleanup
88
+ )
89
+
90
+ from .utils import generate_tab_key, generate_tab_id
91
+
92
+ __all__ = [
93
+ # Models
94
+ 'ProjectState',
95
+ 'FileItem',
96
+ 'TabInfo',
97
+ 'MonitoredFolder',
98
+ 'GitFileChange',
99
+ 'GitDetailedStatus',
100
+
101
+ # Core classes
102
+ 'ProjectStateManager', # Legacy
103
+ 'CentralizedProjectStateManager', # New
104
+ 'CentralizedProjectState', # New
105
+ 'GitStateSnapshot', # New
106
+ 'StateUpdateManager', # New
107
+ 'StateNotificationManager', # New
108
+ 'PeriodicGitMonitor', # New
109
+ 'GitManager',
110
+ 'FileSystemWatcher',
111
+
112
+ # Manager functions (Legacy)
113
+ 'get_or_create_project_state_manager',
114
+ 'reset_global_project_state_manager',
115
+ 'debug_global_manager_state',
116
+
117
+ # Manager functions (New)
118
+ 'get_or_create_centralized_manager',
119
+
120
+ # Legacy Handlers
121
+ 'ProjectStateFolderExpandHandler',
122
+ 'ProjectStateFolderCollapseHandler',
123
+ 'ProjectStateFileOpenHandler',
124
+ 'ProjectStateTabCloseHandler',
125
+ 'ProjectStateSetActiveTabHandler',
126
+ 'ProjectStateDiffOpenHandler',
127
+ 'ProjectStateGitStageHandler',
128
+ 'ProjectStateGitUnstageHandler',
129
+ 'ProjectStateGitRevertHandler',
130
+ 'handle_client_session_cleanup',
131
+
132
+ # New Centralized Handlers
133
+ 'CentralizedProjectStateFolderExpandHandler',
134
+ 'CentralizedProjectStateFolderCollapseHandler',
135
+ 'CentralizedProjectStateFileOpenHandler',
136
+ 'CentralizedProjectStateTabCloseHandler',
137
+ 'CentralizedProjectStateGitStageHandler',
138
+ 'CentralizedProjectStateGitUnstageHandler',
139
+ 'CentralizedProjectStateGitRevertHandler',
140
+ 'handle_centralized_client_session_cleanup',
141
+
142
+ # Utils
143
+ 'generate_tab_key',
144
+ 'generate_tab_id'
145
+ ]
@@ -0,0 +1,299 @@
1
+ """
2
+ Updated handlers that use the centralized project state manager.
3
+
4
+ These handlers replace the old handlers with clean interfaces to the
5
+ centralized state management system.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict
10
+
11
+ from ..base import AsyncHandler
12
+ from .centralized_manager import get_or_create_centralized_manager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CentralizedProjectStateFolderExpandHandler(AsyncHandler):
18
+ """Handler for expanding project folders using centralized state."""
19
+
20
+ @property
21
+ def command_name(self) -> str:
22
+ return "project_state_folder_expand"
23
+
24
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
25
+ """Expand a folder in project state."""
26
+ server_project_id = message.get("project_id")
27
+ folder_path = message.get("folder_path")
28
+ source_client_session = message.get("source_client_session")
29
+
30
+ if not server_project_id:
31
+ raise ValueError("project_id is required")
32
+ if not folder_path:
33
+ raise ValueError("folder_path is required")
34
+ if not source_client_session:
35
+ raise ValueError("source_client_session is required")
36
+
37
+ logger.info("Expanding folder %s for session %s", folder_path, source_client_session)
38
+
39
+ # Get centralized manager
40
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
41
+
42
+ # Expand folder
43
+ success = await manager.expand_folder(source_client_session, folder_path)
44
+
45
+ return {
46
+ "event": "project_state_folder_expand_response",
47
+ "project_id": server_project_id,
48
+ "folder_path": folder_path,
49
+ "success": success
50
+ }
51
+
52
+
53
+ class CentralizedProjectStateFolderCollapseHandler(AsyncHandler):
54
+ """Handler for collapsing project folders using centralized state."""
55
+
56
+ @property
57
+ def command_name(self) -> str:
58
+ return "project_state_folder_collapse"
59
+
60
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
61
+ """Collapse a folder in project state."""
62
+ server_project_id = message.get("project_id")
63
+ folder_path = message.get("folder_path")
64
+ source_client_session = message.get("source_client_session")
65
+
66
+ if not server_project_id:
67
+ raise ValueError("project_id is required")
68
+ if not folder_path:
69
+ raise ValueError("folder_path is required")
70
+ if not source_client_session:
71
+ raise ValueError("source_client_session is required")
72
+
73
+ logger.info("Collapsing folder %s for session %s", folder_path, source_client_session)
74
+
75
+ # Get centralized manager
76
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
77
+
78
+ # Collapse folder
79
+ success = await manager.collapse_folder(source_client_session, folder_path)
80
+
81
+ return {
82
+ "event": "project_state_folder_collapse_response",
83
+ "project_id": server_project_id,
84
+ "folder_path": folder_path,
85
+ "success": success
86
+ }
87
+
88
+
89
+ class CentralizedProjectStateFileOpenHandler(AsyncHandler):
90
+ """Handler for opening files using centralized state."""
91
+
92
+ @property
93
+ def command_name(self) -> str:
94
+ return "project_state_file_open"
95
+
96
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
97
+ """Open a file in project state."""
98
+ server_project_id = message.get("project_id")
99
+ file_path = message.get("file_path")
100
+ source_client_session = message.get("source_client_session")
101
+ set_active = message.get("set_active", True)
102
+
103
+ if not server_project_id:
104
+ raise ValueError("project_id is required")
105
+ if not file_path:
106
+ raise ValueError("file_path is required")
107
+ if not source_client_session:
108
+ raise ValueError("source_client_session is required")
109
+
110
+ logger.info("Opening file %s for session %s", file_path, source_client_session)
111
+
112
+ # Get centralized manager
113
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
114
+
115
+ # Open file
116
+ success = await manager.open_file_tab(source_client_session, file_path, set_active)
117
+
118
+ return {
119
+ "event": "project_state_file_open_response",
120
+ "project_id": server_project_id,
121
+ "file_path": file_path,
122
+ "success": success,
123
+ "set_active": set_active
124
+ }
125
+
126
+
127
+ class CentralizedProjectStateTabCloseHandler(AsyncHandler):
128
+ """Handler for closing tabs using centralized state."""
129
+
130
+ @property
131
+ def command_name(self) -> str:
132
+ return "project_state_tab_close"
133
+
134
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
135
+ """Close a tab in project state."""
136
+ server_project_id = message.get("project_id")
137
+ tab_id = message.get("tab_id")
138
+ source_client_session = message.get("source_client_session")
139
+
140
+ if not server_project_id:
141
+ raise ValueError("project_id is required")
142
+ if not tab_id:
143
+ raise ValueError("tab_id is required")
144
+ if not source_client_session:
145
+ raise ValueError("source_client_session is required")
146
+
147
+ logger.info("Closing tab %s for session %s", tab_id, source_client_session)
148
+
149
+ # Get centralized manager
150
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
151
+
152
+ # Close tab
153
+ success = await manager.close_tab(source_client_session, tab_id)
154
+
155
+ return {
156
+ "event": "project_state_tab_close_response",
157
+ "project_id": server_project_id,
158
+ "tab_id": tab_id,
159
+ "success": success
160
+ }
161
+
162
+
163
+ class CentralizedProjectStateGitStageHandler(AsyncHandler):
164
+ """Handler for staging files using centralized state."""
165
+
166
+ @property
167
+ def command_name(self) -> str:
168
+ return "project_state_git_stage"
169
+
170
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
171
+ """Stage a file in git."""
172
+ server_project_id = message.get("project_id")
173
+ file_path = message.get("file_path")
174
+ source_client_session = message.get("source_client_session")
175
+
176
+ if not server_project_id:
177
+ raise ValueError("project_id is required")
178
+ if not file_path:
179
+ raise ValueError("file_path is required")
180
+ if not source_client_session:
181
+ raise ValueError("source_client_session is required")
182
+
183
+ logger.info("Staging file %s for session %s", file_path, source_client_session)
184
+
185
+ # Get centralized manager
186
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
187
+
188
+ # Stage file
189
+ success = await manager.stage_file(source_client_session, file_path)
190
+
191
+ return {
192
+ "event": "project_state_git_stage_response",
193
+ "project_id": server_project_id,
194
+ "file_path": file_path,
195
+ "success": success
196
+ }
197
+
198
+
199
+ class CentralizedProjectStateGitUnstageHandler(AsyncHandler):
200
+ """Handler for unstaging files using centralized state."""
201
+
202
+ @property
203
+ def command_name(self) -> str:
204
+ return "project_state_git_unstage"
205
+
206
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
207
+ """Unstage a file in git."""
208
+ server_project_id = message.get("project_id")
209
+ file_path = message.get("file_path")
210
+ source_client_session = message.get("source_client_session")
211
+
212
+ if not server_project_id:
213
+ raise ValueError("project_id is required")
214
+ if not file_path:
215
+ raise ValueError("file_path is required")
216
+ if not source_client_session:
217
+ raise ValueError("source_client_session is required")
218
+
219
+ logger.info("Unstaging file %s for session %s", file_path, source_client_session)
220
+
221
+ # Get centralized manager
222
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
223
+
224
+ # Unstage file
225
+ success = await manager.unstage_file(source_client_session, file_path)
226
+
227
+ return {
228
+ "event": "project_state_git_unstage_response",
229
+ "project_id": server_project_id,
230
+ "file_path": file_path,
231
+ "success": success
232
+ }
233
+
234
+
235
+ class CentralizedProjectStateGitRevertHandler(AsyncHandler):
236
+ """Handler for reverting files using centralized state."""
237
+
238
+ @property
239
+ def command_name(self) -> str:
240
+ return "project_state_git_revert"
241
+
242
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
243
+ """Revert a file in git."""
244
+ server_project_id = message.get("project_id")
245
+ file_path = message.get("file_path")
246
+ source_client_session = message.get("source_client_session")
247
+
248
+ if not server_project_id:
249
+ raise ValueError("project_id is required")
250
+ if not file_path:
251
+ raise ValueError("file_path is required")
252
+ if not source_client_session:
253
+ raise ValueError("source_client_session is required")
254
+
255
+ logger.info("Reverting file %s for session %s", file_path, source_client_session)
256
+
257
+ # Get centralized manager
258
+ manager = get_or_create_centralized_manager(self.context, self.control_channel)
259
+
260
+ # Revert file
261
+ success = await manager.revert_file(source_client_session, file_path)
262
+
263
+ return {
264
+ "event": "project_state_git_revert_response",
265
+ "project_id": server_project_id,
266
+ "file_path": file_path,
267
+ "success": success
268
+ }
269
+
270
+
271
+ # Handler for explicit client session cleanup
272
+ async def handle_centralized_client_session_cleanup(handler, payload: Dict[str, Any],
273
+ source_client_session: str) -> Dict[str, Any]:
274
+ """Handle cleanup of a client session using centralized manager."""
275
+ client_session_id = payload.get('client_session_id')
276
+
277
+ if not client_session_id:
278
+ logger.error("client_session_id is required for client session cleanup")
279
+ return {
280
+ "event": "client_session_cleanup_response",
281
+ "success": False,
282
+ "error": "client_session_id is required"
283
+ }
284
+
285
+ logger.info("Handling centralized cleanup for client session: %s", client_session_id)
286
+
287
+ # Get centralized manager
288
+ manager = get_or_create_centralized_manager(handler.context, handler.control_channel)
289
+
290
+ # Clean up the client session
291
+ await manager.cleanup_project_state(client_session_id)
292
+
293
+ logger.info("Centralized client session cleanup completed: %s", client_session_id)
294
+
295
+ return {
296
+ "event": "client_session_cleanup_response",
297
+ "client_session_id": client_session_id,
298
+ "success": True
299
+ }