portacode 0.3.16.dev4__tar.gz → 0.3.16.dev5__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 (44) hide show
  1. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/PKG-INFO +1 -1
  2. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/_version.py +2 -2
  3. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +2 -4
  4. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/project_state_handlers.py +105 -34
  5. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/terminal.py +0 -1
  6. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode.egg-info/PKG-INFO +1 -1
  7. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/.claude/agents/communication-manager.md +0 -0
  8. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/.claude/settings.local.json +0 -0
  9. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/.gitignore +0 -0
  10. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/.gitmodules +0 -0
  11. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/LICENSE +0 -0
  12. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/MANIFEST.in +0 -0
  13. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/Makefile +0 -0
  14. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/README.md +0 -0
  15. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/backup.sh +0 -0
  16. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/docker-compose.yaml +0 -0
  17. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/README.md +0 -0
  18. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/__init__.py +0 -0
  19. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/__main__.py +0 -0
  20. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/cli.py +0 -0
  21. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/README.md +0 -0
  22. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/__init__.py +0 -0
  23. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/client.py +0 -0
  24. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/README.md +0 -0
  25. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/__init__.py +0 -0
  26. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/base.py +0 -0
  27. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/file_handlers.py +0 -0
  28. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/registry.py +0 -0
  29. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/session.py +0 -0
  30. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/system_handlers.py +0 -0
  31. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/handlers/terminal_handlers.py +0 -0
  32. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/connection/multiplex.py +0 -0
  33. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/data.py +0 -0
  34. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/keypair.py +0 -0
  35. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode/service.py +0 -0
  36. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode.egg-info/SOURCES.txt +0 -0
  37. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode.egg-info/dependency_links.txt +0 -0
  38. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode.egg-info/entry_points.txt +0 -0
  39. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode.egg-info/requires.txt +0 -0
  40. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/portacode.egg-info/top_level.txt +0 -0
  41. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/pyproject.toml +0 -0
  42. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/restore.sh +0 -0
  43. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/setup.cfg +0 -0
  44. {portacode-0.3.16.dev4 → portacode-0.3.16.dev5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.16.dev4
3
+ Version: 0.3.16.dev5
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.16.dev4'
21
- __version_tuple__ = version_tuple = (0, 3, 16, 'dev4')
20
+ __version__ = version = '0.3.16.dev5'
21
+ __version_tuple__ = version_tuple = (0, 3, 16, 'dev5')
@@ -545,8 +545,7 @@ Confirms that project state has been successfully initialized for a client sessi
545
545
  * `git_status_summary` (object, optional): Summary of Git status counts (modified, added, deleted, untracked files).
546
546
  * `open_files` (array, mandatory): Array of file paths currently marked as open.
547
547
  * `active_file` (string, optional): Path to the currently active file.
548
- * `expanded_folders` (array, mandatory): Array of folder paths currently expanded.
549
- * `items` (array, mandatory): Array of file/folder items including root level and one level down for all folders. Each folder item has an `is_expanded` boolean property indicating its expansion state.
548
+ * `items` (array, mandatory): 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 has a `parent_path` property indicating which folder it belongs to, and folder items have an `is_expanded` boolean property indicating their expansion state.
550
549
  * `timestamp` (float, mandatory): Unix timestamp of when the state was generated.
551
550
 
552
551
  ### <a name="project_state_update"></a>`project_state_update`
@@ -562,8 +561,7 @@ Sent automatically when project state changes due to file system modifications,
562
561
  * `git_status_summary` (object, optional): Updated summary of Git status counts.
563
562
  * `open_files` (array, mandatory): Updated array of open file paths.
564
563
  * `active_file` (string, optional): Updated active file path.
565
- * `expanded_folders` (array, mandatory): Updated array of expanded folder paths.
566
- * `items` (array, mandatory): Updated array of file/folder items including root level and one level down for all folders. Each folder item has an `is_expanded` boolean property indicating its expansion state.
564
+ * `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 has a `parent_path` property indicating which folder it belongs to, and folder items have an `is_expanded` boolean property indicating their expansion state.
567
565
  * `timestamp` (float, mandatory): Unix timestamp of when the update was generated.
568
566
 
569
567
  ### <a name="project_state_folder_expand_response"></a>`project_state_folder_expand_response`
@@ -43,6 +43,7 @@ class FileItem:
43
43
  name: str
44
44
  path: str
45
45
  is_directory: bool
46
+ parent_path: str
46
47
  size: Optional[int] = None
47
48
  modified_time: Optional[float] = None
48
49
  is_git_tracked: Optional[bool] = None
@@ -64,13 +65,10 @@ class ProjectState:
64
65
  git_status_summary: Optional[Dict[str, int]] = None
65
66
  open_files: Set[str] = None
66
67
  active_file: Optional[str] = None
67
- expanded_folders: Set[str] = None
68
68
 
69
69
  def __post_init__(self):
70
70
  if self.open_files is None:
71
71
  self.open_files = set()
72
- if self.expanded_folders is None:
73
- self.expanded_folders = set()
74
72
 
75
73
 
76
74
  class GitManager:
@@ -275,7 +273,6 @@ class ProjectStateManager:
275
273
  "git_status_summary": state.git_status_summary,
276
274
  "open_files": list(state.open_files),
277
275
  "active_file": state.active_file,
278
- "expanded_folders": list(state.expanded_folders),
279
276
  "items": [self._serialize_file_item(item) for item in state.items]
280
277
  }
281
278
 
@@ -315,8 +312,8 @@ class ProjectStateManager:
315
312
  git_status_summary=git_manager.get_status_summary()
316
313
  )
317
314
 
318
- # Load initial file structure with recursive expanded folder loading
319
- await self._load_expanded_folders_recursively(project_state)
315
+ # Load initial file structure with flattened items
316
+ await self._build_flattened_items_structure(project_state)
320
317
 
321
318
  # Start watching the project folder
322
319
  self.file_watcher.start_watching(project_folder_path)
@@ -379,23 +376,95 @@ class ProjectStateManager:
379
376
  except (OSError, PermissionError) as e:
380
377
  logger.error("Error loading directory %s: %s", directory_path, e)
381
378
 
382
- async def _load_expanded_folders_recursively(self, project_state: ProjectState):
383
- """Load one level down for all expanded folders recursively."""
384
- # First load root level
385
- await self._load_directory_items(project_state, project_state.project_folder_path, is_root=True)
386
-
387
- # Then load one level down for all expanded folders
388
- for folder_path in project_state.expanded_folders:
389
- folder_item = self._find_item_by_path(project_state.items, folder_path)
390
- if folder_item and folder_item.is_directory:
391
- # Load the folder's immediate children
392
- await self._load_directory_items(project_state, folder_path, parent_item=folder_item)
393
-
394
- # Load one level down for each subfolder in this expanded folder
395
- if folder_item.children:
396
- for child in folder_item.children:
397
- if child.is_directory and not child.is_loaded:
398
- await self._load_directory_items(project_state, child.path, parent_item=child)
379
+ async def _build_flattened_items_structure(self, project_state: ProjectState):
380
+ """Build a flattened items structure including all visible items and one level down from expanded folders."""
381
+ all_items = []
382
+
383
+ # Load root items
384
+ root_items = await self._load_directory_items_list(project_state.project_folder_path, project_state.project_folder_path)
385
+
386
+ for item in root_items:
387
+ all_items.append(item)
388
+
389
+ # Always load one level down from root folders (project root is always "expanded")
390
+ # OR if this folder is explicitly expanded, add its children and one level down
391
+ if item.is_directory and (item.parent_path == project_state.project_folder_path or item.is_expanded):
392
+ children = await self._load_directory_items_list(item.path, item.path)
393
+ for child in children:
394
+ all_items.append(child)
395
+
396
+ # If child is a directory, load one level down
397
+ if child.is_directory:
398
+ grandchildren = await self._load_directory_items_list(child.path, child.path)
399
+ all_items.extend(grandchildren)
400
+
401
+ project_state.items = all_items
402
+
403
+ async def _load_directory_items_list(self, directory_path: str, parent_path: str) -> List[FileItem]:
404
+ """Load directory items and return as a list with parent_path."""
405
+ git_manager = None
406
+ for manager in self.git_managers.values():
407
+ if directory_path.startswith(manager.project_path):
408
+ git_manager = manager
409
+ break
410
+
411
+ items = []
412
+
413
+ try:
414
+ with os.scandir(directory_path) as entries:
415
+ for entry in entries:
416
+ try:
417
+ stat_info = entry.stat()
418
+ is_hidden = entry.name.startswith('.')
419
+
420
+ # Get Git status if available
421
+ git_info = {"is_tracked": False, "status": None}
422
+ if git_manager:
423
+ git_info = git_manager.get_file_status(entry.path)
424
+
425
+ # Check if this directory is expanded by finding it in current items
426
+ is_expanded = False
427
+ if entry.is_dir():
428
+ # Check if this folder is expanded by looking for existing items with this path as parent
429
+ is_expanded = self._is_folder_expanded(entry.path)
430
+
431
+ file_item = FileItem(
432
+ name=entry.name,
433
+ path=entry.path,
434
+ is_directory=entry.is_dir(),
435
+ parent_path=parent_path,
436
+ size=stat_info.st_size if entry.is_file() else None,
437
+ modified_time=stat_info.st_mtime,
438
+ is_git_tracked=git_info["is_tracked"],
439
+ git_status=git_info["status"],
440
+ is_hidden=is_hidden,
441
+ is_expanded=is_expanded,
442
+ is_loaded=not entry.is_dir()
443
+ )
444
+
445
+ items.append(file_item)
446
+
447
+ except (OSError, PermissionError) as e:
448
+ logger.debug("Error reading entry %s: %s", entry.path, e)
449
+ continue
450
+
451
+ # Sort items: directories first, then files, both alphabetically
452
+ items.sort(key=lambda x: (not x.is_directory, x.name.lower()))
453
+
454
+ except (OSError, PermissionError) as e:
455
+ logger.error("Error loading directory %s: %s", directory_path, e)
456
+
457
+ return items
458
+
459
+ def _is_folder_expanded(self, folder_path: str) -> bool:
460
+ """Check if a folder is expanded by looking at existing items."""
461
+ # During initial load, no folders are expanded
462
+ # During updates, check if any items have this folder as parent_path
463
+ for project_state in self.projects.values():
464
+ for item in project_state.items:
465
+ if item.parent_path == folder_path:
466
+ return True
467
+ return False
399
468
 
400
469
  async def expand_folder(self, project_id: str, folder_path: str) -> bool:
401
470
  """Expand a folder and load its contents."""
@@ -404,19 +473,18 @@ class ProjectStateManager:
404
473
 
405
474
  project_state = self.projects[project_id]
406
475
 
407
- # Find the folder item
476
+ # Find the folder item and mark it as expanded
408
477
  folder_item = self._find_item_by_path(project_state.items, folder_path)
409
478
  if not folder_item or not folder_item.is_directory:
410
479
  return False
411
480
 
412
481
  folder_item.is_expanded = True
413
- project_state.expanded_folders.add(folder_path)
414
482
 
415
483
  # Start watching this folder
416
484
  self.file_watcher.start_watching(folder_path)
417
485
 
418
- # Reload the entire structure to ensure consistency with recursive loading
419
- await self._load_expanded_folders_recursively(project_state)
486
+ # Rebuild the entire flattened structure to include new expanded content
487
+ await self._build_flattened_items_structure(project_state)
420
488
 
421
489
  self._write_debug_state()
422
490
  return True
@@ -428,18 +496,20 @@ class ProjectStateManager:
428
496
 
429
497
  project_state = self.projects[project_id]
430
498
 
431
- # Find the folder item
499
+ # Find the folder item and mark it as collapsed
432
500
  folder_item = self._find_item_by_path(project_state.items, folder_path)
433
501
  if not folder_item or not folder_item.is_directory:
434
502
  return False
435
503
 
436
504
  folder_item.is_expanded = False
437
- project_state.expanded_folders.discard(folder_path)
438
505
 
439
506
  # Stop watching collapsed folders (except root)
440
507
  if folder_path != project_state.project_folder_path:
441
508
  self.file_watcher.stop_watching(folder_path)
442
509
 
510
+ # Rebuild the flattened structure to remove collapsed content
511
+ await self._build_flattened_items_structure(project_state)
512
+
443
513
  self._write_debug_state()
444
514
  return True
445
515
 
@@ -546,8 +616,8 @@ class ProjectStateManager:
546
616
  self._write_debug_state()
547
617
 
548
618
  async def _reload_visible_structures(self, project_state: ProjectState):
549
- """Reload all visible structures with recursive expanded folder loading."""
550
- await self._load_expanded_folders_recursively(project_state)
619
+ """Reload all visible structures with flattened items."""
620
+ await self._build_flattened_items_structure(project_state)
551
621
 
552
622
  async def _send_project_state_update(self, project_state: ProjectState):
553
623
  """Send project state update to clients."""
@@ -560,7 +630,6 @@ class ProjectStateManager:
560
630
  "git_status_summary": project_state.git_status_summary,
561
631
  "open_files": list(project_state.open_files),
562
632
  "active_file": project_state.active_file,
563
- "expanded_folders": list(project_state.expanded_folders),
564
633
  "items": [self._serialize_file_item(item) for item in project_state.items],
565
634
  "timestamp": time.time()
566
635
  }
@@ -575,8 +644,10 @@ class ProjectStateManager:
575
644
 
576
645
  # Stop watching all folders for this project
577
646
  self.file_watcher.stop_watching(project_state.project_folder_path)
578
- for folder_path in project_state.expanded_folders:
579
- self.file_watcher.stop_watching(folder_path)
647
+ # Stop watching all expanded folders
648
+ for item in project_state.items:
649
+ if item.is_directory and item.is_expanded:
650
+ self.file_watcher.stop_watching(item.path)
580
651
 
581
652
  # Clean up managers
582
653
  self.git_managers.pop(project_id, None)
@@ -460,7 +460,6 @@ class TerminalManager:
460
460
  "git_status_summary": project_state.git_status_summary,
461
461
  "open_files": list(project_state.open_files),
462
462
  "active_file": project_state.active_file,
463
- "expanded_folders": list(project_state.expanded_folders),
464
463
  "items": [manager._serialize_file_item(item) for item in project_state.items],
465
464
  "timestamp": time.time(),
466
465
  "client_sessions": [session_name] # Target this specific session
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.16.dev4
3
+ Version: 0.3.16.dev5
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