portacode 0.3.17.dev1__tar.gz → 0.3.17.dev2__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.
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/.claude/settings.local.json +2 -1
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/PKG-INFO +1 -1
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/_version.py +2 -2
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +13 -2
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/project_state_handlers.py +72 -15
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/.claude/agents/communication-manager.md +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/.gitignore +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/.gitmodules +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/LICENSE +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/MANIFEST.in +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/Makefile +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/README.md +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/backup.sh +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/docker-compose.yaml +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/README.md +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/__init__.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/__main__.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/cli.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/README.md +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/__init__.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/client.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/README.md +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/base.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/registry.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/session.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/multiplex.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/terminal.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/data.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/keypair.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/service.py +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode.egg-info/SOURCES.txt +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode.egg-info/requires.txt +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode.egg-info/top_level.txt +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/pyproject.toml +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/restore.sh +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/setup.cfg +0 -0
- {portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/setup.py +0 -0
|
@@ -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.17.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 3, 17, '
|
|
20
|
+
__version__ = version = '0.3.17.dev2'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 3, 17, 'dev2')
|
{portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
RENAMED
|
@@ -247,6 +247,13 @@ Project state actions manage the state of project folders, including file struct
|
|
|
247
247
|
|
|
248
248
|
**Note:** Project state is automatically initialized when a client session connects with a `project_folder_path` property. No manual initialization command is required.
|
|
249
249
|
|
|
250
|
+
**Tab Management:** Open tabs are internally managed using a dictionary structure with unique keys to prevent duplicates and race conditions:
|
|
251
|
+
- File tabs use `file_path` as the unique key
|
|
252
|
+
- Diff tabs use a composite key: `diff:{file_path}:{from_ref}:{to_ref}:{from_hash}:{to_hash}`
|
|
253
|
+
- Untitled tabs use their `tab_id` as the unique key
|
|
254
|
+
|
|
255
|
+
This ensures that sending the same command multiple times (e.g., `project_state_diff_open` with identical parameters) will not create duplicate tabs but will instead activate the existing tab.
|
|
256
|
+
|
|
250
257
|
### `project_state_folder_expand`
|
|
251
258
|
|
|
252
259
|
Expands a folder in the project tree, loading its contents and enabling monitoring for that folder level. When a folder is expanded, the system proactively loads one level down for all subdirectories to enable immediate expansion in the UI. This action also scans items in the expanded folder and preloads content for any non-empty subdirectories.
|
|
@@ -279,6 +286,8 @@ Collapses a folder in the project tree, stopping monitoring for that folder leve
|
|
|
279
286
|
|
|
280
287
|
Marks a file as open in the project state, tracking it as part of the current editing session.
|
|
281
288
|
|
|
289
|
+
**Duplicate Prevention:** This action prevents creating duplicate file tabs by using the `file_path` as a unique key. If a file tab with the same path already exists, it will be activated instead of creating a new one.
|
|
290
|
+
|
|
282
291
|
**Payload Fields:**
|
|
283
292
|
|
|
284
293
|
* `project_id` (string, mandatory): The project ID from the initialized project state.
|
|
@@ -322,6 +331,8 @@ Sets the currently active tab in the project state. Only one tab can be active a
|
|
|
322
331
|
|
|
323
332
|
Opens a diff tab for comparing file versions at different points in the git timeline. This replaces the previous `project_state_create_diff_tab` action with a more efficient approach that doesn't require the client to provide file content, instead using git timeline references.
|
|
324
333
|
|
|
334
|
+
**Duplicate Prevention:** This action prevents creating duplicate diff tabs by using a unique key based on `file_path`, `from_ref`, `to_ref`, `from_hash`, and `to_hash`. If a diff tab with the same parameters already exists, it will be activated instead of creating a new one.
|
|
335
|
+
|
|
325
336
|
**Payload Fields:**
|
|
326
337
|
|
|
327
338
|
* `project_id` (string, mandatory): The project ID from the initialized project state.
|
|
@@ -578,7 +589,7 @@ Confirms that project state has been successfully initialized for a client sessi
|
|
|
578
589
|
* `is_staged` (boolean): Always true for staged changes.
|
|
579
590
|
* `unstaged_changes` (array, optional): Array of unstaged file changes with same structure as staged_changes but `is_staged` is always false.
|
|
580
591
|
* `untracked_files` (array, optional): Array of untracked files with same structure as staged_changes but `is_staged` is always false and `change_type` is always 'untracked'.
|
|
581
|
-
* `open_tabs` (array, mandatory): Array of tab objects currently open. Each tab object contains:
|
|
592
|
+
* `open_tabs` (array, mandatory): Array of tab objects currently open. Internally stored as a dictionary with unique keys to prevent duplicates, but serialized as an array for API responses. Each tab object contains:
|
|
582
593
|
* `tab_id` (string, mandatory): Unique identifier for the tab.
|
|
583
594
|
* `tab_type` (string, mandatory): Type of tab ("file", "diff", "untitled", "image", "audio", "video").
|
|
584
595
|
* `title` (string, mandatory): Display title for the tab.
|
|
@@ -619,7 +630,7 @@ Sent automatically when project state changes due to file system modifications,
|
|
|
619
630
|
* `git_branch` (string, optional): The current Git branch name if available.
|
|
620
631
|
* `git_status_summary` (object, optional): Updated summary of Git status counts.
|
|
621
632
|
* `git_detailed_status` (object, optional): Updated detailed Git status with comprehensive file change information and content hashes (same structure as in `project_state_initialized`).
|
|
622
|
-
* `open_tabs` (array, mandatory): Updated array of tab objects currently open.
|
|
633
|
+
* `open_tabs` (array, mandatory): Updated array of tab objects currently open. Internally stored as a dictionary with unique keys to prevent duplicates, but serialized as an array for API responses.
|
|
623
634
|
* `active_tab` (object, optional): Updated active tab object.
|
|
624
635
|
* `items` (array, mandatory): Updated flattened array of all visible file/folder items. Always includes root level items and one level down from the project root (since the project root is treated as expanded by default). Also includes items within explicitly expanded folders and one level down from each expanded folder. Each item object contains the following fields:
|
|
625
636
|
* `name` (string, mandatory): The file or directory name.
|
|
@@ -118,12 +118,12 @@ class ProjectState:
|
|
|
118
118
|
git_branch: Optional[str] = None
|
|
119
119
|
git_status_summary: Optional[Dict[str, int]] = None # Kept for backward compatibility
|
|
120
120
|
git_detailed_status: Optional[GitDetailedStatus] = None # New detailed git state
|
|
121
|
-
open_tabs:
|
|
121
|
+
open_tabs: Dict[str, 'TabInfo'] = None # Changed from List to Dict with unique keys
|
|
122
122
|
active_tab: Optional['TabInfo'] = None
|
|
123
123
|
|
|
124
124
|
def __post_init__(self):
|
|
125
125
|
if self.open_tabs is None:
|
|
126
|
-
self.open_tabs =
|
|
126
|
+
self.open_tabs = {}
|
|
127
127
|
if self.monitored_folders is None:
|
|
128
128
|
self.monitored_folders = []
|
|
129
129
|
|
|
@@ -580,7 +580,7 @@ class ProjectStateManager:
|
|
|
580
580
|
"git_branch": state.git_branch,
|
|
581
581
|
"git_status_summary": state.git_status_summary,
|
|
582
582
|
"git_detailed_status": asdict(state.git_detailed_status) if state.git_detailed_status else None,
|
|
583
|
-
"open_tabs": [self._serialize_tab_info(tab) for tab in state.open_tabs],
|
|
583
|
+
"open_tabs": [self._serialize_tab_info(tab) for tab in state.open_tabs.values()],
|
|
584
584
|
"active_tab": self._serialize_tab_info(state.active_tab) if state.active_tab else None,
|
|
585
585
|
"monitored_folders": [asdict(mf) for mf in state.monitored_folders],
|
|
586
586
|
"items": [self._serialize_file_item(item) for item in state.items]
|
|
@@ -953,9 +953,12 @@ class ProjectStateManager:
|
|
|
953
953
|
|
|
954
954
|
project_state = self.projects[client_session_key]
|
|
955
955
|
|
|
956
|
+
# Generate unique key for file tab
|
|
957
|
+
tab_key = generate_tab_key('file', file_path)
|
|
958
|
+
|
|
956
959
|
# Check if file is already open
|
|
957
|
-
|
|
958
|
-
|
|
960
|
+
if tab_key in project_state.open_tabs:
|
|
961
|
+
existing_tab = project_state.open_tabs[tab_key]
|
|
959
962
|
if set_active:
|
|
960
963
|
project_state.active_tab = existing_tab
|
|
961
964
|
self._write_debug_state()
|
|
@@ -967,7 +970,7 @@ class ProjectStateManager:
|
|
|
967
970
|
|
|
968
971
|
try:
|
|
969
972
|
new_tab = await tab_factory.create_file_tab(file_path)
|
|
970
|
-
project_state.open_tabs
|
|
973
|
+
project_state.open_tabs[tab_key] = new_tab
|
|
971
974
|
if set_active:
|
|
972
975
|
project_state.active_tab = new_tab
|
|
973
976
|
|
|
@@ -985,17 +988,25 @@ class ProjectStateManager:
|
|
|
985
988
|
|
|
986
989
|
project_state = self.projects[client_session_key]
|
|
987
990
|
|
|
988
|
-
# Find and remove the tab
|
|
989
|
-
|
|
991
|
+
# Find and remove the tab by searching through the dictionary values
|
|
992
|
+
tab_key_to_remove = None
|
|
993
|
+
tab_to_remove = None
|
|
994
|
+
for key, tab in project_state.open_tabs.items():
|
|
995
|
+
if tab.tab_id == tab_id:
|
|
996
|
+
tab_key_to_remove = key
|
|
997
|
+
tab_to_remove = tab
|
|
998
|
+
break
|
|
999
|
+
|
|
990
1000
|
if not tab_to_remove:
|
|
991
1001
|
return False
|
|
992
1002
|
|
|
993
|
-
project_state.open_tabs
|
|
1003
|
+
del project_state.open_tabs[tab_key_to_remove]
|
|
994
1004
|
|
|
995
1005
|
# Clear active tab if it was the closed tab
|
|
996
1006
|
if project_state.active_tab and project_state.active_tab.tab_id == tab_id:
|
|
997
1007
|
# Set active tab to the last remaining tab, or None if no tabs left
|
|
998
|
-
|
|
1008
|
+
remaining_tabs = list(project_state.open_tabs.values())
|
|
1009
|
+
project_state.active_tab = remaining_tabs[-1] if remaining_tabs else None
|
|
999
1010
|
|
|
1000
1011
|
self._write_debug_state()
|
|
1001
1012
|
return True
|
|
@@ -1008,8 +1019,12 @@ class ProjectStateManager:
|
|
|
1008
1019
|
project_state = self.projects[client_session_key]
|
|
1009
1020
|
|
|
1010
1021
|
if tab_id:
|
|
1011
|
-
# Find the tab by ID
|
|
1012
|
-
tab =
|
|
1022
|
+
# Find the tab by ID in the dictionary values
|
|
1023
|
+
tab = None
|
|
1024
|
+
for t in project_state.open_tabs.values():
|
|
1025
|
+
if t.tab_id == tab_id:
|
|
1026
|
+
tab = t
|
|
1027
|
+
break
|
|
1013
1028
|
if not tab:
|
|
1014
1029
|
return False
|
|
1015
1030
|
project_state.active_tab = tab
|
|
@@ -1033,6 +1048,19 @@ class ProjectStateManager:
|
|
|
1033
1048
|
logger.error("Cannot create diff tab: not a git repository")
|
|
1034
1049
|
return False
|
|
1035
1050
|
|
|
1051
|
+
# Generate unique key for diff tab
|
|
1052
|
+
tab_key = generate_tab_key('diff', file_path,
|
|
1053
|
+
from_ref=from_ref, to_ref=to_ref,
|
|
1054
|
+
from_hash=from_hash, to_hash=to_hash)
|
|
1055
|
+
|
|
1056
|
+
# Check if this diff tab is already open
|
|
1057
|
+
if tab_key in project_state.open_tabs:
|
|
1058
|
+
existing_tab = project_state.open_tabs[tab_key]
|
|
1059
|
+
project_state.active_tab = existing_tab
|
|
1060
|
+
logger.info(f"Diff tab already exists, activating: {tab_key}")
|
|
1061
|
+
self._write_debug_state()
|
|
1062
|
+
return True
|
|
1063
|
+
|
|
1036
1064
|
try:
|
|
1037
1065
|
# Get content based on the reference type
|
|
1038
1066
|
original_content = ""
|
|
@@ -1103,7 +1131,7 @@ class ProjectStateManager:
|
|
|
1103
1131
|
'diff_timeline': True
|
|
1104
1132
|
})
|
|
1105
1133
|
|
|
1106
|
-
project_state.open_tabs
|
|
1134
|
+
project_state.open_tabs[tab_key] = diff_tab
|
|
1107
1135
|
project_state.active_tab = diff_tab
|
|
1108
1136
|
|
|
1109
1137
|
logger.info(f"Created timeline diff tab for: {file_path} ({from_ref} → {to_ref})")
|
|
@@ -1197,7 +1225,7 @@ class ProjectStateManager:
|
|
|
1197
1225
|
"git_branch": project_state.git_branch,
|
|
1198
1226
|
"git_status_summary": project_state.git_status_summary,
|
|
1199
1227
|
"git_detailed_status": str(project_state.git_detailed_status) if project_state.git_detailed_status else None,
|
|
1200
|
-
"open_tabs": tuple((tab.tab_id, tab.tab_type, tab.title) for tab in project_state.open_tabs),
|
|
1228
|
+
"open_tabs": tuple((tab.tab_id, tab.tab_type, tab.title) for tab in project_state.open_tabs.values()),
|
|
1201
1229
|
"active_tab": project_state.active_tab.tab_id if project_state.active_tab else None,
|
|
1202
1230
|
"items_count": len(project_state.items),
|
|
1203
1231
|
"monitored_folders": tuple((mf.folder_path, mf.is_expanded) for mf in sorted(project_state.monitored_folders, key=lambda x: x.folder_path))
|
|
@@ -1221,7 +1249,7 @@ class ProjectStateManager:
|
|
|
1221
1249
|
"git_branch": project_state.git_branch,
|
|
1222
1250
|
"git_status_summary": project_state.git_status_summary,
|
|
1223
1251
|
"git_detailed_status": asdict(project_state.git_detailed_status) if project_state.git_detailed_status else None,
|
|
1224
|
-
"open_tabs": [self._serialize_tab_info(tab) for tab in project_state.open_tabs],
|
|
1252
|
+
"open_tabs": [self._serialize_tab_info(tab) for tab in project_state.open_tabs.values()],
|
|
1225
1253
|
"active_tab": self._serialize_tab_info(project_state.active_tab) if project_state.active_tab else None,
|
|
1226
1254
|
"items": [self._serialize_file_item(item) for item in project_state.items],
|
|
1227
1255
|
"timestamp": time.time(),
|
|
@@ -1274,6 +1302,35 @@ class ProjectStateManager:
|
|
|
1274
1302
|
logger.info("Cleaned up %d project states", len(keys_to_remove))
|
|
1275
1303
|
|
|
1276
1304
|
|
|
1305
|
+
def generate_tab_key(tab_type: str, file_path: str, **kwargs) -> str:
|
|
1306
|
+
"""Generate a unique key for a tab.
|
|
1307
|
+
|
|
1308
|
+
Args:
|
|
1309
|
+
tab_type: Type of tab ('file', 'diff', 'untitled', etc.)
|
|
1310
|
+
file_path: Path to the file
|
|
1311
|
+
**kwargs: Additional parameters for diff tabs (from_ref, to_ref, from_hash, to_hash)
|
|
1312
|
+
|
|
1313
|
+
Returns:
|
|
1314
|
+
Unique string key for the tab
|
|
1315
|
+
"""
|
|
1316
|
+
import uuid
|
|
1317
|
+
|
|
1318
|
+
if tab_type == 'file':
|
|
1319
|
+
return file_path
|
|
1320
|
+
elif tab_type == 'diff':
|
|
1321
|
+
from_ref = kwargs.get('from_ref', '')
|
|
1322
|
+
to_ref = kwargs.get('to_ref', '')
|
|
1323
|
+
from_hash = kwargs.get('from_hash', '')
|
|
1324
|
+
to_hash = kwargs.get('to_hash', '')
|
|
1325
|
+
return f"diff:{file_path}:{from_ref}:{to_ref}:{from_hash}:{to_hash}"
|
|
1326
|
+
elif tab_type == 'untitled':
|
|
1327
|
+
# For untitled tabs, use the tab_id as the key since they don't have a file path
|
|
1328
|
+
return kwargs.get('tab_id', str(uuid.uuid4()))
|
|
1329
|
+
else:
|
|
1330
|
+
# For other tab types, use file_path if available, otherwise tab_id
|
|
1331
|
+
return file_path if file_path else kwargs.get('tab_id', str(uuid.uuid4()))
|
|
1332
|
+
|
|
1333
|
+
|
|
1277
1334
|
# Helper function for other handlers to get/create project state manager
|
|
1278
1335
|
def _get_or_create_project_state_manager(context: Dict[str, Any], control_channel) -> 'ProjectStateManager':
|
|
1279
1336
|
"""Get or create project state manager with debug setup."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/file_handlers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/system_handlers.py
RENAMED
|
File without changes
|
{portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/tab_factory.py
RENAMED
|
File without changes
|
{portacode-0.3.17.dev1 → portacode-0.3.17.dev2}/portacode/connection/handlers/terminal_handlers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|