portacode 0.3.16.dev3__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.dev3 → portacode-0.3.16.dev5}/PKG-INFO +1 -1
  2. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/_version.py +2 -2
  3. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +2 -4
  4. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/project_state_handlers.py +106 -35
  5. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/terminal.py +0 -1
  6. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode.egg-info/PKG-INFO +1 -1
  7. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/.claude/agents/communication-manager.md +0 -0
  8. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/.claude/settings.local.json +0 -0
  9. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/.gitignore +0 -0
  10. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/.gitmodules +0 -0
  11. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/LICENSE +0 -0
  12. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/MANIFEST.in +0 -0
  13. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/Makefile +0 -0
  14. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/README.md +0 -0
  15. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/backup.sh +0 -0
  16. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/docker-compose.yaml +0 -0
  17. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/README.md +0 -0
  18. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/__init__.py +0 -0
  19. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/__main__.py +0 -0
  20. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/cli.py +0 -0
  21. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/README.md +0 -0
  22. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/__init__.py +0 -0
  23. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/client.py +0 -0
  24. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/README.md +0 -0
  25. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/__init__.py +0 -0
  26. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/base.py +0 -0
  27. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/file_handlers.py +0 -0
  28. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/registry.py +0 -0
  29. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/session.py +0 -0
  30. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/system_handlers.py +0 -0
  31. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/handlers/terminal_handlers.py +0 -0
  32. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/connection/multiplex.py +0 -0
  33. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/data.py +0 -0
  34. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/keypair.py +0 -0
  35. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode/service.py +0 -0
  36. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode.egg-info/SOURCES.txt +0 -0
  37. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode.egg-info/dependency_links.txt +0 -0
  38. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode.egg-info/entry_points.txt +0 -0
  39. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode.egg-info/requires.txt +0 -0
  40. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/portacode.egg-info/top_level.txt +0 -0
  41. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/pyproject.toml +0 -0
  42. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/restore.sh +0 -0
  43. {portacode-0.3.16.dev3 → portacode-0.3.16.dev5}/setup.cfg +0 -0
  44. {portacode-0.3.16.dev3 → 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.dev3
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.dev3'
21
- __version_tuple__ = version_tuple = (0, 3, 16, 'dev3')
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
319
- await self._load_directory_items(project_state, project_folder_path, is_root=True)
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)
@@ -372,10 +369,6 @@ class ProjectStateManager:
372
369
 
373
370
  if is_root:
374
371
  project_state.items = items
375
- # Load one level down for folders to enable immediate expansion
376
- for item in items:
377
- if item.is_directory:
378
- await self._load_directory_items(project_state, item.path, parent_item=item)
379
372
  elif parent_item:
380
373
  parent_item.children = items
381
374
  parent_item.is_loaded = True
@@ -383,6 +376,96 @@ class ProjectStateManager:
383
376
  except (OSError, PermissionError) as e:
384
377
  logger.error("Error loading directory %s: %s", directory_path, e)
385
378
 
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
468
+
386
469
  async def expand_folder(self, project_id: str, folder_path: str) -> bool:
387
470
  """Expand a folder and load its contents."""
388
471
  if project_id not in self.projects:
@@ -390,26 +473,18 @@ class ProjectStateManager:
390
473
 
391
474
  project_state = self.projects[project_id]
392
475
 
393
- # Find the folder item
476
+ # Find the folder item and mark it as expanded
394
477
  folder_item = self._find_item_by_path(project_state.items, folder_path)
395
478
  if not folder_item or not folder_item.is_directory:
396
479
  return False
397
480
 
398
- # Load children if not already loaded
399
- if not folder_item.is_loaded:
400
- await self._load_directory_items(project_state, folder_path, parent_item=folder_item)
401
-
402
481
  folder_item.is_expanded = True
403
- project_state.expanded_folders.add(folder_path)
404
482
 
405
483
  # Start watching this folder
406
484
  self.file_watcher.start_watching(folder_path)
407
485
 
408
- # Preload one level down for newly expanded folders
409
- if folder_item.children:
410
- for child in folder_item.children:
411
- if child.is_directory and not child.is_loaded:
412
- await self._load_directory_items(project_state, child.path, parent_item=child)
486
+ # Rebuild the entire flattened structure to include new expanded content
487
+ await self._build_flattened_items_structure(project_state)
413
488
 
414
489
  self._write_debug_state()
415
490
  return True
@@ -421,18 +496,20 @@ class ProjectStateManager:
421
496
 
422
497
  project_state = self.projects[project_id]
423
498
 
424
- # Find the folder item
499
+ # Find the folder item and mark it as collapsed
425
500
  folder_item = self._find_item_by_path(project_state.items, folder_path)
426
501
  if not folder_item or not folder_item.is_directory:
427
502
  return False
428
503
 
429
504
  folder_item.is_expanded = False
430
- project_state.expanded_folders.discard(folder_path)
431
505
 
432
506
  # Stop watching collapsed folders (except root)
433
507
  if folder_path != project_state.project_folder_path:
434
508
  self.file_watcher.stop_watching(folder_path)
435
509
 
510
+ # Rebuild the flattened structure to remove collapsed content
511
+ await self._build_flattened_items_structure(project_state)
512
+
436
513
  self._write_debug_state()
437
514
  return True
438
515
 
@@ -539,15 +616,8 @@ class ProjectStateManager:
539
616
  self._write_debug_state()
540
617
 
541
618
  async def _reload_visible_structures(self, project_state: ProjectState):
542
- """Reload only visible (expanded) directory structures."""
543
- # Reload root
544
- await self._load_directory_items(project_state, project_state.project_folder_path, is_root=True)
545
-
546
- # Reload expanded folders
547
- for folder_path in project_state.expanded_folders:
548
- folder_item = self._find_item_by_path(project_state.items, folder_path)
549
- if folder_item and folder_item.is_directory:
550
- await self._load_directory_items(project_state, folder_path, parent_item=folder_item)
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.dev3
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