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.
- portacode/_version.py +16 -3
- portacode/cli.py +155 -19
- portacode/connection/client.py +152 -12
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +1577 -0
- portacode/connection/handlers/__init__.py +43 -1
- portacode/connection/handlers/base.py +122 -18
- portacode/connection/handlers/chunked_content.py +244 -0
- portacode/connection/handlers/diff_handlers.py +603 -0
- portacode/connection/handlers/file_handlers.py +902 -17
- portacode/connection/handlers/project_aware_file_handlers.py +226 -0
- portacode/connection/handlers/project_state/README.md +312 -0
- portacode/connection/handlers/project_state/__init__.py +92 -0
- portacode/connection/handlers/project_state/file_system_watcher.py +179 -0
- portacode/connection/handlers/project_state/git_manager.py +1502 -0
- portacode/connection/handlers/project_state/handlers.py +875 -0
- portacode/connection/handlers/project_state/manager.py +1331 -0
- portacode/connection/handlers/project_state/models.py +108 -0
- portacode/connection/handlers/project_state/utils.py +50 -0
- portacode/connection/handlers/project_state_handlers.py +45 -0
- portacode/connection/handlers/proxmox_infra.py +307 -0
- portacode/connection/handlers/registry.py +53 -10
- portacode/connection/handlers/session.py +705 -53
- portacode/connection/handlers/system_handlers.py +142 -8
- portacode/connection/handlers/tab_factory.py +389 -0
- portacode/connection/handlers/terminal_handlers.py +150 -11
- portacode/connection/handlers/update_handler.py +61 -0
- portacode/connection/multiplex.py +60 -2
- portacode/connection/terminal.py +695 -28
- portacode/keypair.py +63 -1
- portacode/link_capture/__init__.py +38 -0
- portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- portacode/link_capture/bin/elinks +3 -0
- portacode/link_capture/bin/gio-open +3 -0
- portacode/link_capture/bin/gnome-open +3 -0
- portacode/link_capture/bin/gvfs-open +3 -0
- portacode/link_capture/bin/kde-open +3 -0
- portacode/link_capture/bin/kfmclient +3 -0
- portacode/link_capture/bin/link_capture_exec.sh +11 -0
- portacode/link_capture/bin/link_capture_wrapper.py +75 -0
- portacode/link_capture/bin/links +3 -0
- portacode/link_capture/bin/links2 +3 -0
- portacode/link_capture/bin/lynx +3 -0
- portacode/link_capture/bin/mate-open +3 -0
- portacode/link_capture/bin/netsurf +3 -0
- portacode/link_capture/bin/sensible-browser +3 -0
- portacode/link_capture/bin/w3m +3 -0
- portacode/link_capture/bin/x-www-browser +3 -0
- portacode/link_capture/bin/xdg-open +3 -0
- portacode/logging_categories.py +140 -0
- portacode/pairing.py +103 -0
- portacode/service.py +6 -0
- portacode/static/js/test-ntp-clock.html +63 -0
- portacode/static/js/utils/ntp-clock.js +232 -0
- portacode/utils/NTP_ARCHITECTURE.md +136 -0
- portacode/utils/__init__.py +1 -0
- portacode/utils/diff_apply.py +456 -0
- portacode/utils/diff_renderer.py +371 -0
- portacode/utils/ntp_clock.py +65 -0
- portacode-1.4.11.dev0.dist-info/METADATA +298 -0
- portacode-1.4.11.dev0.dist-info/RECORD +97 -0
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/WHEEL +1 -1
- portacode-1.4.11.dev0.dist-info/top_level.txt +3 -0
- test_modules/README.md +296 -0
- test_modules/__init__.py +1 -0
- test_modules/test_device_online.py +44 -0
- test_modules/test_file_operations.py +743 -0
- test_modules/test_git_status_ui.py +370 -0
- test_modules/test_login_flow.py +50 -0
- test_modules/test_navigate_testing_folder.py +361 -0
- test_modules/test_play_store_screenshots.py +294 -0
- test_modules/test_terminal_buffer_performance.py +261 -0
- test_modules/test_terminal_interaction.py +80 -0
- test_modules/test_terminal_loading_race_condition.py +95 -0
- test_modules/test_terminal_start.py +56 -0
- testing_framework/.env.example +21 -0
- testing_framework/README.md +334 -0
- testing_framework/__init__.py +17 -0
- testing_framework/cli.py +326 -0
- testing_framework/core/__init__.py +1 -0
- testing_framework/core/base_test.py +336 -0
- testing_framework/core/cli_manager.py +177 -0
- testing_framework/core/hierarchical_runner.py +577 -0
- testing_framework/core/playwright_manager.py +520 -0
- testing_framework/core/runner.py +447 -0
- testing_framework/core/shared_cli_manager.py +234 -0
- testing_framework/core/test_discovery.py +112 -0
- testing_framework/requirements.txt +12 -0
- portacode-0.3.4.dev0.dist-info/METADATA +0 -236
- portacode-0.3.4.dev0.dist-info/RECORD +0 -27
- portacode-0.3.4.dev0.dist-info/top_level.txt +0 -1
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/entry_points.txt +0 -0
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
"""Request handlers for project state management operations.
|
|
2
|
+
|
|
3
|
+
This module contains all the AsyncHandler classes that handle different
|
|
4
|
+
project state operations like folder expansion/collapse, file operations,
|
|
5
|
+
tab management, and git operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
|
|
11
|
+
from ..base import AsyncHandler
|
|
12
|
+
from ..chunked_content import create_chunked_response
|
|
13
|
+
from .manager import get_or_create_project_state_manager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProjectStateFolderExpandHandler(AsyncHandler):
|
|
19
|
+
"""Handler for expanding project folders."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def command_name(self) -> str:
|
|
23
|
+
return "project_state_folder_expand"
|
|
24
|
+
|
|
25
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
26
|
+
"""Expand a folder in project state."""
|
|
27
|
+
logger.info("ProjectStateFolderExpandHandler.execute called with message: %s", message)
|
|
28
|
+
|
|
29
|
+
server_project_id = message.get("project_id") # Server-side UUID (for response)
|
|
30
|
+
folder_path = message.get("folder_path")
|
|
31
|
+
source_client_session = message.get("source_client_session") # This is our key
|
|
32
|
+
|
|
33
|
+
logger.info("Extracted server_project_id: %s, folder_path: %s, source_client_session: %s",
|
|
34
|
+
server_project_id, folder_path, source_client_session)
|
|
35
|
+
|
|
36
|
+
if not server_project_id:
|
|
37
|
+
raise ValueError("project_id is required")
|
|
38
|
+
if not folder_path:
|
|
39
|
+
raise ValueError("folder_path is required")
|
|
40
|
+
if not source_client_session:
|
|
41
|
+
raise ValueError("source_client_session is required")
|
|
42
|
+
|
|
43
|
+
logger.info("Getting project state manager...")
|
|
44
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
45
|
+
logger.info("Got manager: %s", manager)
|
|
46
|
+
|
|
47
|
+
# With the new design, client session ID maps directly to project state
|
|
48
|
+
if source_client_session not in manager.projects:
|
|
49
|
+
logger.error("No project state found for client session: %s. Available project states: %s",
|
|
50
|
+
source_client_session, list(manager.projects.keys()))
|
|
51
|
+
response = {
|
|
52
|
+
"event": "project_state_folder_expand_response",
|
|
53
|
+
"project_id": server_project_id,
|
|
54
|
+
"folder_path": folder_path,
|
|
55
|
+
"success": False,
|
|
56
|
+
"error": f"No project state found for client session: {source_client_session}"
|
|
57
|
+
}
|
|
58
|
+
logger.error("Returning error response: %s", response)
|
|
59
|
+
return response
|
|
60
|
+
|
|
61
|
+
logger.info("Found project state for client session: %s", source_client_session)
|
|
62
|
+
|
|
63
|
+
logger.info("Calling manager.expand_folder...")
|
|
64
|
+
success = await manager.expand_folder(source_client_session, folder_path)
|
|
65
|
+
logger.info("expand_folder returned: %s", success)
|
|
66
|
+
|
|
67
|
+
if success:
|
|
68
|
+
# Send updated state
|
|
69
|
+
logger.info("Sending project state update...")
|
|
70
|
+
project_state = manager.projects[source_client_session]
|
|
71
|
+
await manager._send_project_state_update(project_state, server_project_id)
|
|
72
|
+
logger.info("Project state update sent")
|
|
73
|
+
|
|
74
|
+
response = {
|
|
75
|
+
"event": "project_state_folder_expand_response",
|
|
76
|
+
"project_id": server_project_id, # Return the server-side project ID
|
|
77
|
+
"folder_path": folder_path,
|
|
78
|
+
"success": success
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.info("Returning response: %s", response)
|
|
82
|
+
return response
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ProjectStateFolderCollapseHandler(AsyncHandler):
|
|
86
|
+
"""Handler for collapsing project folders."""
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def command_name(self) -> str:
|
|
90
|
+
return "project_state_folder_collapse"
|
|
91
|
+
|
|
92
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
93
|
+
"""Collapse a folder in project state."""
|
|
94
|
+
server_project_id = message.get("project_id") # Server-side UUID (for response)
|
|
95
|
+
folder_path = message.get("folder_path")
|
|
96
|
+
source_client_session = message.get("source_client_session") # This is our key
|
|
97
|
+
|
|
98
|
+
if not server_project_id:
|
|
99
|
+
raise ValueError("project_id is required")
|
|
100
|
+
if not folder_path:
|
|
101
|
+
raise ValueError("folder_path is required")
|
|
102
|
+
if not source_client_session:
|
|
103
|
+
raise ValueError("source_client_session is required")
|
|
104
|
+
|
|
105
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
106
|
+
|
|
107
|
+
# Find project state using client session
|
|
108
|
+
if source_client_session not in manager.projects:
|
|
109
|
+
logger.error("No project state found for client session: %s. Available project states: %s",
|
|
110
|
+
source_client_session, list(manager.projects.keys()))
|
|
111
|
+
return {
|
|
112
|
+
"event": "project_state_folder_collapse_response",
|
|
113
|
+
"project_id": server_project_id,
|
|
114
|
+
"folder_path": folder_path,
|
|
115
|
+
"success": False,
|
|
116
|
+
"error": f"No project state found for client session: {source_client_session}"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
success = await manager.collapse_folder(source_client_session, folder_path)
|
|
120
|
+
|
|
121
|
+
if success:
|
|
122
|
+
# Send updated state
|
|
123
|
+
project_state = manager.projects[source_client_session]
|
|
124
|
+
await manager._send_project_state_update(project_state, server_project_id)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"event": "project_state_folder_collapse_response",
|
|
128
|
+
"project_id": server_project_id, # Return the server-side project ID
|
|
129
|
+
"folder_path": folder_path,
|
|
130
|
+
"success": success
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ProjectStateFileOpenHandler(AsyncHandler):
|
|
135
|
+
"""Handler for opening files in project state."""
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def command_name(self) -> str:
|
|
139
|
+
return "project_state_file_open"
|
|
140
|
+
|
|
141
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
142
|
+
"""Open a file in project state."""
|
|
143
|
+
server_project_id = message.get("project_id") # Server-side UUID (for response)
|
|
144
|
+
file_path = message.get("file_path")
|
|
145
|
+
source_client_session = message.get("source_client_session") # This is our key
|
|
146
|
+
set_active = message.get("set_active", True)
|
|
147
|
+
|
|
148
|
+
if not server_project_id:
|
|
149
|
+
raise ValueError("project_id is required")
|
|
150
|
+
if not file_path:
|
|
151
|
+
raise ValueError("file_path is required")
|
|
152
|
+
if not source_client_session:
|
|
153
|
+
raise ValueError("source_client_session is required")
|
|
154
|
+
|
|
155
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
156
|
+
|
|
157
|
+
# Find project state using client session
|
|
158
|
+
if source_client_session not in manager.projects:
|
|
159
|
+
logger.error("No project state found for client session: %s. Available project states: %s",
|
|
160
|
+
source_client_session, list(manager.projects.keys()))
|
|
161
|
+
return {
|
|
162
|
+
"event": "project_state_file_open_response",
|
|
163
|
+
"project_id": server_project_id,
|
|
164
|
+
"file_path": file_path,
|
|
165
|
+
"success": False,
|
|
166
|
+
"set_active": set_active,
|
|
167
|
+
"error": f"No project state found for client session: {source_client_session}"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
success = await manager.open_file(source_client_session, file_path, set_active)
|
|
171
|
+
|
|
172
|
+
if success:
|
|
173
|
+
# Send updated state
|
|
174
|
+
project_state = manager.projects[source_client_session]
|
|
175
|
+
await manager._send_project_state_update(project_state, server_project_id)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"event": "project_state_file_open_response",
|
|
179
|
+
"project_id": server_project_id, # Return the server-side project ID
|
|
180
|
+
"file_path": file_path,
|
|
181
|
+
"success": success,
|
|
182
|
+
"set_active": set_active
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ProjectStateTabCloseHandler(AsyncHandler):
|
|
187
|
+
"""Handler for closing tabs in project state."""
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def command_name(self) -> str:
|
|
191
|
+
return "project_state_tab_close"
|
|
192
|
+
|
|
193
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
194
|
+
"""Close a tab in project state."""
|
|
195
|
+
server_project_id = message.get("project_id") # Server-side UUID (for response)
|
|
196
|
+
tab_id = message.get("tab_id")
|
|
197
|
+
source_client_session = message.get("source_client_session") # This is our key
|
|
198
|
+
|
|
199
|
+
if not server_project_id:
|
|
200
|
+
raise ValueError("project_id is required")
|
|
201
|
+
if not tab_id:
|
|
202
|
+
raise ValueError("tab_id is required")
|
|
203
|
+
if not source_client_session:
|
|
204
|
+
raise ValueError("source_client_session is required")
|
|
205
|
+
|
|
206
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
207
|
+
|
|
208
|
+
# Find project state using client session
|
|
209
|
+
if source_client_session not in manager.projects:
|
|
210
|
+
logger.error("No project state found for client session: %s. Available project states: %s",
|
|
211
|
+
source_client_session, list(manager.projects.keys()))
|
|
212
|
+
return {
|
|
213
|
+
"event": "project_state_tab_close_response",
|
|
214
|
+
"project_id": server_project_id,
|
|
215
|
+
"tab_id": tab_id,
|
|
216
|
+
"success": False,
|
|
217
|
+
"error": f"No project state found for client session: {source_client_session}"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
success = await manager.close_tab(source_client_session, tab_id)
|
|
221
|
+
|
|
222
|
+
if success:
|
|
223
|
+
# Send updated state
|
|
224
|
+
project_state = manager.projects[source_client_session]
|
|
225
|
+
await manager._send_project_state_update(project_state, server_project_id)
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"event": "project_state_tab_close_response",
|
|
229
|
+
"project_id": server_project_id, # Return the server-side project ID
|
|
230
|
+
"tab_id": tab_id,
|
|
231
|
+
"success": success
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ProjectStateSetActiveTabHandler(AsyncHandler):
|
|
236
|
+
"""Handler for setting active tab in project state."""
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def command_name(self) -> str:
|
|
240
|
+
return "project_state_set_active_tab"
|
|
241
|
+
|
|
242
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
243
|
+
"""Set active tab in project state."""
|
|
244
|
+
server_project_id = message.get("project_id") # Server-side UUID (for response)
|
|
245
|
+
tab_id = message.get("tab_id") # Can be None to clear active tab
|
|
246
|
+
source_client_session = message.get("source_client_session") # This is our key
|
|
247
|
+
|
|
248
|
+
if not server_project_id:
|
|
249
|
+
raise ValueError("project_id is required")
|
|
250
|
+
if not source_client_session:
|
|
251
|
+
raise ValueError("source_client_session is required")
|
|
252
|
+
|
|
253
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
254
|
+
|
|
255
|
+
# Find project state using client session
|
|
256
|
+
if source_client_session not in manager.projects:
|
|
257
|
+
logger.error("No project state found for client session: %s. Available project states: %s",
|
|
258
|
+
source_client_session, list(manager.projects.keys()))
|
|
259
|
+
return {
|
|
260
|
+
"event": "project_state_set_active_tab_response",
|
|
261
|
+
"project_id": server_project_id,
|
|
262
|
+
"tab_id": tab_id,
|
|
263
|
+
"success": False,
|
|
264
|
+
"error": f"No project state found for client session: {source_client_session}"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
success = await manager.set_active_tab(source_client_session, tab_id)
|
|
268
|
+
|
|
269
|
+
if success:
|
|
270
|
+
# Send updated state
|
|
271
|
+
project_state = manager.projects[source_client_session]
|
|
272
|
+
await manager._send_project_state_update(project_state, server_project_id)
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
"event": "project_state_set_active_tab_response",
|
|
276
|
+
"project_id": server_project_id, # Return the server-side project ID
|
|
277
|
+
"tab_id": tab_id,
|
|
278
|
+
"success": success
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ProjectStateDiffOpenHandler(AsyncHandler):
|
|
283
|
+
"""Handler for opening diff tabs based on git timeline references."""
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def command_name(self) -> str:
|
|
287
|
+
return "project_state_diff_open"
|
|
288
|
+
|
|
289
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
290
|
+
"""Open a diff tab comparing file versions at different git timeline points."""
|
|
291
|
+
server_project_id = message.get("project_id") # Server-side UUID (for response)
|
|
292
|
+
file_path = message.get("file_path")
|
|
293
|
+
from_ref = message.get("from_ref") # 'head', 'staged', 'working', 'commit'
|
|
294
|
+
to_ref = message.get("to_ref") # 'head', 'staged', 'working', 'commit'
|
|
295
|
+
from_hash = message.get("from_hash") # Optional commit hash for from_ref='commit'
|
|
296
|
+
to_hash = message.get("to_hash") # Optional commit hash for to_ref='commit'
|
|
297
|
+
source_client_session = message.get("source_client_session") # This is our key
|
|
298
|
+
|
|
299
|
+
if not server_project_id:
|
|
300
|
+
raise ValueError("project_id is required")
|
|
301
|
+
if not file_path:
|
|
302
|
+
raise ValueError("file_path is required")
|
|
303
|
+
if not from_ref:
|
|
304
|
+
raise ValueError("from_ref is required")
|
|
305
|
+
if not to_ref:
|
|
306
|
+
raise ValueError("to_ref is required")
|
|
307
|
+
if not source_client_session:
|
|
308
|
+
raise ValueError("source_client_session is required")
|
|
309
|
+
|
|
310
|
+
# Validate reference types
|
|
311
|
+
valid_refs = {'head', 'staged', 'working', 'commit'}
|
|
312
|
+
if from_ref not in valid_refs:
|
|
313
|
+
raise ValueError(f"Invalid from_ref: {from_ref}. Must be one of {valid_refs}")
|
|
314
|
+
if to_ref not in valid_refs:
|
|
315
|
+
raise ValueError(f"Invalid to_ref: {to_ref}. Must be one of {valid_refs}")
|
|
316
|
+
|
|
317
|
+
# Validate commit hashes are provided when needed
|
|
318
|
+
if from_ref == 'commit' and not from_hash:
|
|
319
|
+
raise ValueError("from_hash is required when from_ref='commit'")
|
|
320
|
+
if to_ref == 'commit' and not to_hash:
|
|
321
|
+
raise ValueError("to_hash is required when to_ref='commit'")
|
|
322
|
+
|
|
323
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
324
|
+
|
|
325
|
+
# Find project state using client session
|
|
326
|
+
if source_client_session not in manager.projects:
|
|
327
|
+
logger.error("No project state found for client session: %s. Available project states: %s",
|
|
328
|
+
source_client_session, list(manager.projects.keys()))
|
|
329
|
+
return {
|
|
330
|
+
"event": "project_state_diff_open_response",
|
|
331
|
+
"project_id": server_project_id,
|
|
332
|
+
"file_path": file_path,
|
|
333
|
+
"from_ref": from_ref,
|
|
334
|
+
"to_ref": to_ref,
|
|
335
|
+
"success": False,
|
|
336
|
+
"error": f"No project state found for client session: {source_client_session}"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
success = await manager.open_diff_tab(
|
|
340
|
+
source_client_session, file_path, from_ref, to_ref, from_hash, to_hash
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if success:
|
|
344
|
+
# Send updated state
|
|
345
|
+
project_state = manager.projects[source_client_session]
|
|
346
|
+
await manager._send_project_state_update(project_state, server_project_id)
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
"event": "project_state_diff_open_response",
|
|
350
|
+
"project_id": server_project_id, # Return the server-side project ID
|
|
351
|
+
"file_path": file_path,
|
|
352
|
+
"from_ref": from_ref,
|
|
353
|
+
"to_ref": to_ref,
|
|
354
|
+
"from_hash": from_hash,
|
|
355
|
+
"to_hash": to_hash,
|
|
356
|
+
"success": success
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class ProjectStateGitStageHandler(AsyncHandler):
|
|
361
|
+
"""Handler for staging files in git for a project."""
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def command_name(self) -> str:
|
|
365
|
+
return "project_state_git_stage"
|
|
366
|
+
|
|
367
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
368
|
+
"""Stage file(s) in git for a project. Supports both single file and bulk operations."""
|
|
369
|
+
server_project_id = message.get("project_id")
|
|
370
|
+
file_path = message.get("file_path") # Single file (backward compatibility)
|
|
371
|
+
file_paths = message.get("file_paths") # Multiple files (bulk operation)
|
|
372
|
+
stage_all = message.get("stage_all", False) # Stage all changes
|
|
373
|
+
source_client_session = message.get("source_client_session")
|
|
374
|
+
|
|
375
|
+
if not server_project_id:
|
|
376
|
+
raise ValueError("project_id is required")
|
|
377
|
+
if not source_client_session:
|
|
378
|
+
raise ValueError("source_client_session is required")
|
|
379
|
+
|
|
380
|
+
# Determine operation mode
|
|
381
|
+
if stage_all:
|
|
382
|
+
operation_desc = "staging all changes"
|
|
383
|
+
file_paths_to_stage = []
|
|
384
|
+
elif file_paths:
|
|
385
|
+
operation_desc = f"staging {len(file_paths)} files"
|
|
386
|
+
file_paths_to_stage = file_paths
|
|
387
|
+
elif file_path:
|
|
388
|
+
operation_desc = f"staging file {file_path}"
|
|
389
|
+
file_paths_to_stage = [file_path]
|
|
390
|
+
else:
|
|
391
|
+
raise ValueError("Either file_path, file_paths, or stage_all must be provided")
|
|
392
|
+
|
|
393
|
+
logger.info("%s for project %s (client session: %s)",
|
|
394
|
+
operation_desc.capitalize(), server_project_id, source_client_session)
|
|
395
|
+
|
|
396
|
+
# Get the project state manager
|
|
397
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
398
|
+
|
|
399
|
+
# Get git manager for the client session
|
|
400
|
+
git_manager = manager.git_managers.get(source_client_session)
|
|
401
|
+
if not git_manager:
|
|
402
|
+
raise ValueError("No git repository found for this project")
|
|
403
|
+
|
|
404
|
+
# Perform the staging operation
|
|
405
|
+
if stage_all:
|
|
406
|
+
success = git_manager.stage_all_changes()
|
|
407
|
+
elif len(file_paths_to_stage) == 1:
|
|
408
|
+
success = git_manager.stage_file(file_paths_to_stage[0])
|
|
409
|
+
else:
|
|
410
|
+
success = git_manager.stage_files(file_paths_to_stage)
|
|
411
|
+
|
|
412
|
+
if success:
|
|
413
|
+
# Refresh git status only (no filesystem changes from staging)
|
|
414
|
+
await manager._refresh_project_state(
|
|
415
|
+
source_client_session,
|
|
416
|
+
git_only=True,
|
|
417
|
+
reason="git_stage",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Build response
|
|
421
|
+
response = {
|
|
422
|
+
"event": "project_state_git_stage_response",
|
|
423
|
+
"project_id": server_project_id,
|
|
424
|
+
"success": success
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
# Include appropriate file information in response for backward compatibility
|
|
428
|
+
if file_path:
|
|
429
|
+
response["file_path"] = file_path
|
|
430
|
+
if file_paths:
|
|
431
|
+
response["file_paths"] = file_paths
|
|
432
|
+
if stage_all:
|
|
433
|
+
response["stage_all"] = True
|
|
434
|
+
|
|
435
|
+
return response
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class ProjectStateGitUnstageHandler(AsyncHandler):
|
|
439
|
+
"""Handler for unstaging files in git for a project."""
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def command_name(self) -> str:
|
|
443
|
+
return "project_state_git_unstage"
|
|
444
|
+
|
|
445
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
446
|
+
"""Unstage file(s) in git for a project. Supports both single file and bulk operations."""
|
|
447
|
+
server_project_id = message.get("project_id")
|
|
448
|
+
file_path = message.get("file_path") # Single file (backward compatibility)
|
|
449
|
+
file_paths = message.get("file_paths") # Multiple files (bulk operation)
|
|
450
|
+
unstage_all = message.get("unstage_all", False) # Unstage all changes
|
|
451
|
+
source_client_session = message.get("source_client_session")
|
|
452
|
+
|
|
453
|
+
if not server_project_id:
|
|
454
|
+
raise ValueError("project_id is required")
|
|
455
|
+
if not source_client_session:
|
|
456
|
+
raise ValueError("source_client_session is required")
|
|
457
|
+
|
|
458
|
+
# Determine operation mode
|
|
459
|
+
if unstage_all:
|
|
460
|
+
operation_desc = "unstaging all changes"
|
|
461
|
+
file_paths_to_unstage = []
|
|
462
|
+
elif file_paths:
|
|
463
|
+
operation_desc = f"unstaging {len(file_paths)} files"
|
|
464
|
+
file_paths_to_unstage = file_paths
|
|
465
|
+
elif file_path:
|
|
466
|
+
operation_desc = f"unstaging file {file_path}"
|
|
467
|
+
file_paths_to_unstage = [file_path]
|
|
468
|
+
else:
|
|
469
|
+
raise ValueError("Either file_path, file_paths, or unstage_all must be provided")
|
|
470
|
+
|
|
471
|
+
logger.info("%s for project %s (client session: %s)",
|
|
472
|
+
operation_desc.capitalize(), server_project_id, source_client_session)
|
|
473
|
+
|
|
474
|
+
# Get the project state manager
|
|
475
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
476
|
+
|
|
477
|
+
# Get git manager for the client session
|
|
478
|
+
git_manager = manager.git_managers.get(source_client_session)
|
|
479
|
+
if not git_manager:
|
|
480
|
+
raise ValueError("No git repository found for this project")
|
|
481
|
+
|
|
482
|
+
# Perform the unstaging operation
|
|
483
|
+
if unstage_all:
|
|
484
|
+
success = git_manager.unstage_all_changes()
|
|
485
|
+
elif len(file_paths_to_unstage) == 1:
|
|
486
|
+
success = git_manager.unstage_file(file_paths_to_unstage[0])
|
|
487
|
+
else:
|
|
488
|
+
success = git_manager.unstage_files(file_paths_to_unstage)
|
|
489
|
+
|
|
490
|
+
if success:
|
|
491
|
+
# Refresh git status only (no filesystem changes from unstaging)
|
|
492
|
+
await manager._refresh_project_state(
|
|
493
|
+
source_client_session,
|
|
494
|
+
git_only=True,
|
|
495
|
+
reason="git_unstage",
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Build response
|
|
499
|
+
response = {
|
|
500
|
+
"event": "project_state_git_unstage_response",
|
|
501
|
+
"project_id": server_project_id,
|
|
502
|
+
"success": success
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Include appropriate file information in response for backward compatibility
|
|
506
|
+
if file_path:
|
|
507
|
+
response["file_path"] = file_path
|
|
508
|
+
if file_paths:
|
|
509
|
+
response["file_paths"] = file_paths
|
|
510
|
+
if unstage_all:
|
|
511
|
+
response["unstage_all"] = True
|
|
512
|
+
|
|
513
|
+
return response
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class ProjectStateGitRevertHandler(AsyncHandler):
|
|
517
|
+
"""Handler for reverting files in git for a project."""
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def command_name(self) -> str:
|
|
521
|
+
return "project_state_git_revert"
|
|
522
|
+
|
|
523
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
524
|
+
"""Revert file(s) in git for a project. Supports both single file and bulk operations."""
|
|
525
|
+
server_project_id = message.get("project_id")
|
|
526
|
+
file_path = message.get("file_path") # Single file (backward compatibility)
|
|
527
|
+
file_paths = message.get("file_paths") # Multiple files (bulk operation)
|
|
528
|
+
revert_all = message.get("revert_all", False) # Revert all changes
|
|
529
|
+
source_client_session = message.get("source_client_session")
|
|
530
|
+
|
|
531
|
+
if not server_project_id:
|
|
532
|
+
raise ValueError("project_id is required")
|
|
533
|
+
if not source_client_session:
|
|
534
|
+
raise ValueError("source_client_session is required")
|
|
535
|
+
|
|
536
|
+
# Determine operation mode
|
|
537
|
+
if revert_all:
|
|
538
|
+
operation_desc = "reverting all changes"
|
|
539
|
+
file_paths_to_revert = []
|
|
540
|
+
elif file_paths:
|
|
541
|
+
operation_desc = f"reverting {len(file_paths)} files"
|
|
542
|
+
file_paths_to_revert = file_paths
|
|
543
|
+
elif file_path:
|
|
544
|
+
operation_desc = f"reverting file {file_path}"
|
|
545
|
+
file_paths_to_revert = [file_path]
|
|
546
|
+
else:
|
|
547
|
+
raise ValueError("Either file_path, file_paths, or revert_all must be provided")
|
|
548
|
+
|
|
549
|
+
logger.info("%s for project %s (client session: %s)",
|
|
550
|
+
operation_desc.capitalize(), server_project_id, source_client_session)
|
|
551
|
+
|
|
552
|
+
# Get the project state manager
|
|
553
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
554
|
+
|
|
555
|
+
# Get git manager for the client session
|
|
556
|
+
git_manager = manager.git_managers.get(source_client_session)
|
|
557
|
+
if not git_manager:
|
|
558
|
+
raise ValueError("No git repository found for this project")
|
|
559
|
+
|
|
560
|
+
# Perform the revert operation
|
|
561
|
+
if revert_all:
|
|
562
|
+
success = git_manager.revert_all_changes()
|
|
563
|
+
elif len(file_paths_to_revert) == 1:
|
|
564
|
+
success = git_manager.revert_file(file_paths_to_revert[0])
|
|
565
|
+
else:
|
|
566
|
+
success = git_manager.revert_files(file_paths_to_revert)
|
|
567
|
+
|
|
568
|
+
if success:
|
|
569
|
+
# Refresh entire project state to ensure consistency
|
|
570
|
+
await manager._refresh_project_state(
|
|
571
|
+
source_client_session,
|
|
572
|
+
reason="git_revert",
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Build response
|
|
576
|
+
response = {
|
|
577
|
+
"event": "project_state_git_revert_response",
|
|
578
|
+
"project_id": server_project_id,
|
|
579
|
+
"success": success
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
# Include appropriate file information in response for backward compatibility
|
|
583
|
+
if file_path:
|
|
584
|
+
response["file_path"] = file_path
|
|
585
|
+
if file_paths:
|
|
586
|
+
response["file_paths"] = file_paths
|
|
587
|
+
if revert_all:
|
|
588
|
+
response["revert_all"] = True
|
|
589
|
+
|
|
590
|
+
return response
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class ProjectStateGitCommitHandler(AsyncHandler):
|
|
594
|
+
"""Handler for committing staged changes in git for a project."""
|
|
595
|
+
|
|
596
|
+
@property
|
|
597
|
+
def command_name(self) -> str:
|
|
598
|
+
return "project_state_git_commit"
|
|
599
|
+
|
|
600
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
601
|
+
"""Commit staged changes with the given commit message."""
|
|
602
|
+
server_project_id = message.get("project_id")
|
|
603
|
+
commit_message = message.get("commit_message")
|
|
604
|
+
source_client_session = message.get("source_client_session")
|
|
605
|
+
|
|
606
|
+
if not server_project_id:
|
|
607
|
+
raise ValueError("project_id is required")
|
|
608
|
+
if not commit_message:
|
|
609
|
+
raise ValueError("commit_message is required")
|
|
610
|
+
if not source_client_session:
|
|
611
|
+
raise ValueError("source_client_session is required")
|
|
612
|
+
|
|
613
|
+
logger.info("Committing changes for project %s (client session: %s) with message: %s",
|
|
614
|
+
server_project_id, source_client_session, commit_message[:50] + "..." if len(commit_message) > 50 else commit_message)
|
|
615
|
+
|
|
616
|
+
# Get the project state manager
|
|
617
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
618
|
+
|
|
619
|
+
# Get git manager for the client session
|
|
620
|
+
git_manager = manager.git_managers.get(source_client_session)
|
|
621
|
+
if not git_manager:
|
|
622
|
+
raise ValueError("No git repository found for this project")
|
|
623
|
+
|
|
624
|
+
# Commit the staged changes
|
|
625
|
+
success = False
|
|
626
|
+
error_message = None
|
|
627
|
+
commit_hash = None
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
success = git_manager.commit_changes(commit_message)
|
|
631
|
+
if success:
|
|
632
|
+
# Get the commit hash of the new commit
|
|
633
|
+
commit_hash = git_manager.get_head_commit_hash()
|
|
634
|
+
|
|
635
|
+
# Refresh git status only (no filesystem changes from commit)
|
|
636
|
+
await manager._refresh_project_state(
|
|
637
|
+
source_client_session,
|
|
638
|
+
git_only=True,
|
|
639
|
+
reason="git_commit",
|
|
640
|
+
)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
error_message = str(e)
|
|
643
|
+
logger.error("Error during commit: %s", error_message)
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
"event": "project_state_git_commit_response",
|
|
647
|
+
"project_id": server_project_id,
|
|
648
|
+
"commit_message": commit_message,
|
|
649
|
+
"success": success,
|
|
650
|
+
"error": error_message,
|
|
651
|
+
"commit_hash": commit_hash
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# Handler for explicit client session cleanup
|
|
656
|
+
async def handle_client_session_cleanup(handler, payload: Dict[str, Any], source_client_session: str) -> Dict[str, Any]:
|
|
657
|
+
"""Handle explicit cleanup of a client session when server notifies of permanent disconnection."""
|
|
658
|
+
client_session_id = payload.get('client_session_id')
|
|
659
|
+
|
|
660
|
+
if not client_session_id:
|
|
661
|
+
logger.error("client_session_id is required for client session cleanup")
|
|
662
|
+
return {
|
|
663
|
+
"event": "client_session_cleanup_response",
|
|
664
|
+
"success": False,
|
|
665
|
+
"error": "client_session_id is required"
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
logger.info("Handling explicit cleanup for client session: %s", client_session_id)
|
|
669
|
+
|
|
670
|
+
# Get the project state manager
|
|
671
|
+
manager = get_or_create_project_state_manager(handler.context, handler.control_channel)
|
|
672
|
+
|
|
673
|
+
# Clean up the client session's project state
|
|
674
|
+
await manager.cleanup_projects_by_client_session(client_session_id)
|
|
675
|
+
|
|
676
|
+
logger.info("Client session cleanup completed: %s", client_session_id)
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
"event": "client_session_cleanup_response",
|
|
680
|
+
"client_session_id": client_session_id,
|
|
681
|
+
"success": True
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class ProjectStateDiffContentHandler(AsyncHandler):
|
|
686
|
+
"""Handler for requesting specific diff content for diff tabs."""
|
|
687
|
+
|
|
688
|
+
@property
|
|
689
|
+
def command_name(self) -> str:
|
|
690
|
+
return "project_state_diff_content_request"
|
|
691
|
+
|
|
692
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
693
|
+
"""Request specific content for a diff tab (original, modified, or html_diff)."""
|
|
694
|
+
server_project_id = message.get("project_id")
|
|
695
|
+
file_path = message.get("file_path")
|
|
696
|
+
from_ref = message.get("from_ref")
|
|
697
|
+
to_ref = message.get("to_ref")
|
|
698
|
+
from_hash = message.get("from_hash")
|
|
699
|
+
to_hash = message.get("to_hash")
|
|
700
|
+
content_type = message.get("content_type") # 'original', 'modified', 'html_diff'
|
|
701
|
+
source_client_session = message.get("source_client_session")
|
|
702
|
+
|
|
703
|
+
# Validate required fields
|
|
704
|
+
if not server_project_id:
|
|
705
|
+
raise ValueError("project_id is required")
|
|
706
|
+
if not file_path:
|
|
707
|
+
raise ValueError("file_path is required")
|
|
708
|
+
if not from_ref:
|
|
709
|
+
raise ValueError("from_ref is required")
|
|
710
|
+
if not to_ref:
|
|
711
|
+
raise ValueError("to_ref is required")
|
|
712
|
+
if not content_type:
|
|
713
|
+
raise ValueError("content_type is required")
|
|
714
|
+
if not source_client_session:
|
|
715
|
+
raise ValueError("source_client_session is required")
|
|
716
|
+
|
|
717
|
+
# Validate content_type
|
|
718
|
+
valid_content_types = ["original", "modified", "html_diff", "all"]
|
|
719
|
+
if content_type not in valid_content_types:
|
|
720
|
+
raise ValueError(f"content_type must be one of: {valid_content_types}")
|
|
721
|
+
|
|
722
|
+
# Get the project state manager
|
|
723
|
+
manager = get_or_create_project_state_manager(self.context, self.control_channel)
|
|
724
|
+
|
|
725
|
+
# Get the project state for this client session
|
|
726
|
+
if source_client_session not in manager.projects:
|
|
727
|
+
raise ValueError(f"No project state found for client session: {source_client_session}")
|
|
728
|
+
|
|
729
|
+
project_state = manager.projects[source_client_session]
|
|
730
|
+
|
|
731
|
+
try:
|
|
732
|
+
# Find the diff tab with matching parameters
|
|
733
|
+
matching_tab = None
|
|
734
|
+
for tab in project_state.open_tabs.values():
|
|
735
|
+
if tab.tab_type == "diff" and tab.file_path == file_path:
|
|
736
|
+
# Get diff parameters from metadata
|
|
737
|
+
tab_metadata = getattr(tab, 'metadata', {}) or {}
|
|
738
|
+
tab_from_ref = tab_metadata.get('from_ref')
|
|
739
|
+
tab_to_ref = tab_metadata.get('to_ref')
|
|
740
|
+
tab_from_hash = tab_metadata.get('from_hash')
|
|
741
|
+
tab_to_hash = tab_metadata.get('to_hash')
|
|
742
|
+
|
|
743
|
+
if (tab_from_ref == from_ref and
|
|
744
|
+
tab_to_ref == to_ref and
|
|
745
|
+
tab_from_hash == from_hash and
|
|
746
|
+
tab_to_hash == to_hash):
|
|
747
|
+
matching_tab = tab
|
|
748
|
+
break
|
|
749
|
+
|
|
750
|
+
if not matching_tab:
|
|
751
|
+
# Debug information
|
|
752
|
+
logger.error(f"No diff tab found for file_path={file_path}, from_ref={from_ref}, to_ref={to_ref}")
|
|
753
|
+
logger.error(f"Available diff tabs: {[(tab.file_path, getattr(tab, 'metadata', {})) for tab in project_state.open_tabs.values() if tab.tab_type == 'diff']}")
|
|
754
|
+
raise ValueError(f"No diff tab found matching the specified parameters: file_path={file_path}, from_ref={from_ref}, to_ref={to_ref}")
|
|
755
|
+
|
|
756
|
+
# Get the requested content based on type
|
|
757
|
+
content = None
|
|
758
|
+
if content_type == "original":
|
|
759
|
+
content = matching_tab.original_content
|
|
760
|
+
elif content_type == "modified":
|
|
761
|
+
content = matching_tab.modified_content
|
|
762
|
+
elif content_type == "html_diff":
|
|
763
|
+
# For html_diff, we need to get the HTML diff versions from metadata
|
|
764
|
+
html_diff_versions = getattr(matching_tab, 'metadata', {}).get('html_diff_versions')
|
|
765
|
+
if html_diff_versions:
|
|
766
|
+
import json
|
|
767
|
+
content = json.dumps(html_diff_versions)
|
|
768
|
+
elif content_type == "all":
|
|
769
|
+
# Return all content types as a JSON object
|
|
770
|
+
html_diff_versions = getattr(matching_tab, 'metadata', {}).get('html_diff_versions')
|
|
771
|
+
import json
|
|
772
|
+
content = json.dumps({
|
|
773
|
+
"original_content": matching_tab.original_content,
|
|
774
|
+
"modified_content": matching_tab.modified_content,
|
|
775
|
+
"html_diff_versions": html_diff_versions
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
# If content is None or incomplete for "all", regenerate if needed
|
|
779
|
+
if content is None or (content_type == "all" and not all([matching_tab.original_content, matching_tab.modified_content])):
|
|
780
|
+
if content_type in ["original", "modified", "all"]:
|
|
781
|
+
# Re-generate the diff content if needed
|
|
782
|
+
await manager.open_diff_tab(
|
|
783
|
+
source_client_session,
|
|
784
|
+
file_path,
|
|
785
|
+
from_ref,
|
|
786
|
+
to_ref,
|
|
787
|
+
from_hash,
|
|
788
|
+
to_hash
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Try to get content again after regeneration (use same matching logic)
|
|
792
|
+
updated_tab = None
|
|
793
|
+
for tab in project_state.open_tabs.values():
|
|
794
|
+
if tab.tab_type == "diff" and tab.file_path == file_path:
|
|
795
|
+
tab_metadata = getattr(tab, 'metadata', {}) or {}
|
|
796
|
+
if (tab_metadata.get('from_ref') == from_ref and
|
|
797
|
+
tab_metadata.get('to_ref') == to_ref and
|
|
798
|
+
tab_metadata.get('from_hash') == from_hash and
|
|
799
|
+
tab_metadata.get('to_hash') == to_hash):
|
|
800
|
+
updated_tab = tab
|
|
801
|
+
break
|
|
802
|
+
|
|
803
|
+
if updated_tab:
|
|
804
|
+
if content_type == "original":
|
|
805
|
+
content = updated_tab.original_content
|
|
806
|
+
elif content_type == "modified":
|
|
807
|
+
content = updated_tab.modified_content
|
|
808
|
+
elif content_type == "html_diff":
|
|
809
|
+
html_diff_versions = getattr(updated_tab, 'metadata', {}).get('html_diff_versions')
|
|
810
|
+
if html_diff_versions:
|
|
811
|
+
import json
|
|
812
|
+
content = json.dumps(html_diff_versions)
|
|
813
|
+
elif content_type == "all":
|
|
814
|
+
html_diff_versions = getattr(updated_tab, 'metadata', {}).get('html_diff_versions')
|
|
815
|
+
import json
|
|
816
|
+
content = json.dumps({
|
|
817
|
+
"original_content": updated_tab.original_content,
|
|
818
|
+
"modified_content": updated_tab.modified_content,
|
|
819
|
+
"html_diff_versions": html_diff_versions
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
success = content is not None
|
|
823
|
+
base_response = {
|
|
824
|
+
"event": "project_state_diff_content_response",
|
|
825
|
+
"project_id": server_project_id,
|
|
826
|
+
"file_path": file_path,
|
|
827
|
+
"from_ref": from_ref,
|
|
828
|
+
"to_ref": to_ref,
|
|
829
|
+
"content_type": content_type,
|
|
830
|
+
"success": success
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
# Add request_id if present in original message
|
|
834
|
+
if "request_id" in message:
|
|
835
|
+
base_response["request_id"] = message["request_id"]
|
|
836
|
+
|
|
837
|
+
if from_hash:
|
|
838
|
+
base_response["from_hash"] = from_hash
|
|
839
|
+
if to_hash:
|
|
840
|
+
base_response["to_hash"] = to_hash
|
|
841
|
+
|
|
842
|
+
if success:
|
|
843
|
+
# Create chunked responses for large content
|
|
844
|
+
responses = create_chunked_response(base_response, "content", content)
|
|
845
|
+
|
|
846
|
+
# Send all responses
|
|
847
|
+
for response in responses:
|
|
848
|
+
await self.send_response(response, project_id=server_project_id)
|
|
849
|
+
|
|
850
|
+
logger.info(f"Sent diff content response in {len(responses)} chunk(s) for {content_type} content")
|
|
851
|
+
else:
|
|
852
|
+
base_response["error"] = f"Failed to load {content_type} content for diff"
|
|
853
|
+
base_response["chunked"] = False
|
|
854
|
+
await self.send_response(base_response, project_id=server_project_id)
|
|
855
|
+
|
|
856
|
+
return # AsyncHandler doesn't return responses, it sends them
|
|
857
|
+
|
|
858
|
+
except Exception as e:
|
|
859
|
+
logger.error("Error processing diff content request: %s", e)
|
|
860
|
+
error_response = {
|
|
861
|
+
"event": "project_state_diff_content_response",
|
|
862
|
+
"project_id": server_project_id,
|
|
863
|
+
"file_path": file_path,
|
|
864
|
+
"from_ref": from_ref,
|
|
865
|
+
"to_ref": to_ref,
|
|
866
|
+
"content_type": content_type,
|
|
867
|
+
"success": False,
|
|
868
|
+
"error": str(e),
|
|
869
|
+
"chunked": False
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
# Add request_id if present in original message
|
|
873
|
+
if "request_id" in message:
|
|
874
|
+
error_response["request_id"] = message["request_id"]
|
|
875
|
+
await self.send_response(error_response, project_id=server_project_id)
|