portacode 0.3.16.dev7__tar.gz → 0.3.16.dev9__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.dev7 → portacode-0.3.16.dev9}/PKG-INFO +1 -1
  2. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/_version.py +2 -2
  3. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +28 -2
  4. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/project_state_handlers.py +104 -38
  5. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode.egg-info/PKG-INFO +1 -1
  6. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/.claude/agents/communication-manager.md +0 -0
  7. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/.claude/settings.local.json +0 -0
  8. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/.gitignore +0 -0
  9. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/.gitmodules +0 -0
  10. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/LICENSE +0 -0
  11. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/MANIFEST.in +0 -0
  12. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/Makefile +0 -0
  13. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/README.md +0 -0
  14. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/backup.sh +0 -0
  15. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/docker-compose.yaml +0 -0
  16. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/README.md +0 -0
  17. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/__init__.py +0 -0
  18. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/__main__.py +0 -0
  19. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/cli.py +0 -0
  20. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/README.md +0 -0
  21. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/__init__.py +0 -0
  22. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/client.py +0 -0
  23. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/README.md +0 -0
  24. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/__init__.py +0 -0
  25. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/base.py +0 -0
  26. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/file_handlers.py +0 -0
  27. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/registry.py +0 -0
  28. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/session.py +0 -0
  29. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/system_handlers.py +0 -0
  30. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/handlers/terminal_handlers.py +0 -0
  31. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/multiplex.py +0 -0
  32. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/connection/terminal.py +0 -0
  33. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/data.py +0 -0
  34. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/keypair.py +0 -0
  35. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode/service.py +0 -0
  36. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode.egg-info/SOURCES.txt +0 -0
  37. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode.egg-info/dependency_links.txt +0 -0
  38. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode.egg-info/entry_points.txt +0 -0
  39. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode.egg-info/requires.txt +0 -0
  40. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/portacode.egg-info/top_level.txt +0 -0
  41. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/pyproject.toml +0 -0
  42. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/restore.sh +0 -0
  43. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/setup.cfg +0 -0
  44. {portacode-0.3.16.dev7 → portacode-0.3.16.dev9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.16.dev7
3
+ Version: 0.3.16.dev9
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.dev7'
21
- __version_tuple__ = version_tuple = (0, 3, 16, 'dev7')
20
+ __version__ = version = '0.3.16.dev9'
21
+ __version_tuple__ = version_tuple = (0, 3, 16, 'dev9')
@@ -545,7 +545,20 @@ 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
- * `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.
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 object contains the following fields:
549
+ * `name` (string, mandatory): The file or directory name.
550
+ * `path` (string, mandatory): The absolute path to the file or directory.
551
+ * `is_directory` (boolean, mandatory): Whether this item is a directory.
552
+ * `parent_path` (string, mandatory): The absolute path to the parent directory.
553
+ * `size` (integer, optional): File size in bytes. Only present for files, not directories.
554
+ * `modified_time` (float, optional): Last modification time as Unix timestamp.
555
+ * `is_git_tracked` (boolean, optional): Whether the file is tracked by Git. Only present if project is a Git repository.
556
+ * `git_status` (string, optional): Git status of the file ("clean", "modified", "untracked", "ignored"). Only present if project is a Git repository.
557
+ * `is_hidden` (boolean, mandatory): Whether the file/directory name starts with a dot (hidden file).
558
+ * `is_ignored` (boolean, mandatory): Whether the file is ignored by Git. Only meaningful if project is a Git repository.
559
+ * `children` (array, optional): Array of child FileItem objects for directories. Usually null in flattened structure as children are included as separate items.
560
+ * `is_expanded` (boolean, mandatory): Whether this directory is expanded in the project tree. Only meaningful for directories.
561
+ * `is_loaded` (boolean, mandatory): Whether the directory contents have been loaded. Always true for files, indicates loading state for directories.
549
562
  * `timestamp` (float, mandatory): Unix timestamp of when the state was generated.
550
563
 
551
564
  ### <a name="project_state_update"></a>`project_state_update`
@@ -561,7 +574,20 @@ Sent automatically when project state changes due to file system modifications,
561
574
  * `git_status_summary` (object, optional): Updated summary of Git status counts.
562
575
  * `open_files` (array, mandatory): Updated array of open file paths.
563
576
  * `active_file` (string, optional): Updated active file path.
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.
577
+ * `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:
578
+ * `name` (string, mandatory): The file or directory name.
579
+ * `path` (string, mandatory): The absolute path to the file or directory.
580
+ * `is_directory` (boolean, mandatory): Whether this item is a directory.
581
+ * `parent_path` (string, mandatory): The absolute path to the parent directory.
582
+ * `size` (integer, optional): File size in bytes. Only present for files, not directories.
583
+ * `modified_time` (float, optional): Last modification time as Unix timestamp.
584
+ * `is_git_tracked` (boolean, optional): Whether the file is tracked by Git. Only present if project is a Git repository.
585
+ * `git_status` (string, optional): Git status of the file ("clean", "modified", "untracked", "ignored"). Only present if project is a Git repository.
586
+ * `is_hidden` (boolean, mandatory): Whether the file/directory name starts with a dot (hidden file).
587
+ * `is_ignored` (boolean, mandatory): Whether the file is ignored by Git. Only meaningful if project is a Git repository.
588
+ * `children` (array, optional): Array of child FileItem objects for directories. Usually null in flattened structure as children are included as separate items.
589
+ * `is_expanded` (boolean, mandatory): Whether this directory is expanded in the project tree. Only meaningful for directories.
590
+ * `is_loaded` (boolean, mandatory): Whether the directory contents have been loaded. Always true for files, indicates loading state for directories.
565
591
  * `timestamp` (float, mandatory): Unix timestamp of when the update was generated.
566
592
 
567
593
  ### <a name="project_state_folder_expand_response"></a>`project_state_folder_expand_response`
@@ -37,6 +37,12 @@ except ImportError:
37
37
  logger = logging.getLogger(__name__)
38
38
 
39
39
 
40
+ @dataclass
41
+ class MonitoredFolder:
42
+ """Represents a folder that is being monitored for changes."""
43
+ folder_path: str
44
+ is_expanded: bool = False
45
+
40
46
  @dataclass
41
47
  class FileItem:
42
48
  """Represents a file or directory item with metadata."""
@@ -61,6 +67,7 @@ class ProjectState:
61
67
  project_id: str
62
68
  project_folder_path: str
63
69
  items: List[FileItem]
70
+ monitored_folders: List[MonitoredFolder] = None
64
71
  is_git_repo: bool = False
65
72
  git_branch: Optional[str] = None
66
73
  git_status_summary: Optional[Dict[str, int]] = None
@@ -70,6 +77,8 @@ class ProjectState:
70
77
  def __post_init__(self):
71
78
  if self.open_files is None:
72
79
  self.open_files = set()
80
+ if self.monitored_folders is None:
81
+ self.monitored_folders = []
73
82
 
74
83
 
75
84
  class GitManager:
@@ -285,6 +294,7 @@ class ProjectStateManager:
285
294
  "git_status_summary": state.git_status_summary,
286
295
  "open_files": list(state.open_files),
287
296
  "active_file": state.active_file,
297
+ "monitored_folders": [asdict(mf) for mf in state.monitored_folders],
288
298
  "items": [self._serialize_file_item(item) for item in state.items]
289
299
  }
290
300
 
@@ -324,17 +334,71 @@ class ProjectStateManager:
324
334
  git_status_summary=git_manager.get_status_summary()
325
335
  )
326
336
 
337
+ # Initialize monitored folders with project root and its immediate subdirectories
338
+ await self._initialize_monitored_folders(project_state)
339
+
327
340
  # Load initial file structure with flattened items
328
341
  await self._build_flattened_items_structure(project_state)
329
342
 
330
- # Start watching the project folder
331
- self.file_watcher.start_watching(project_folder_path)
343
+ # Start watching all monitored folders
344
+ await self._start_watching_monitored_folders(project_state)
332
345
 
333
346
  self.projects[project_id] = project_state
334
347
  self._write_debug_state()
335
348
 
336
349
  return project_state
337
350
 
351
+ async def _initialize_monitored_folders(self, project_state: ProjectState):
352
+ """Initialize monitored folders with project root (expanded) and its immediate subdirectories (collapsed)."""
353
+ # Add project root as expanded
354
+ project_state.monitored_folders.append(
355
+ MonitoredFolder(folder_path=project_state.project_folder_path, is_expanded=True)
356
+ )
357
+
358
+ # Scan project root for immediate subdirectories and add them as collapsed
359
+ try:
360
+ with os.scandir(project_state.project_folder_path) as entries:
361
+ for entry in entries:
362
+ if entry.is_dir() and entry.name != '.git' and not entry.name.startswith('.'):
363
+ project_state.monitored_folders.append(
364
+ MonitoredFolder(folder_path=entry.path, is_expanded=False)
365
+ )
366
+ except (OSError, PermissionError) as e:
367
+ logger.error("Error scanning project root for subdirectories: %s", e)
368
+
369
+ async def _start_watching_monitored_folders(self, project_state: ProjectState):
370
+ """Start watching all monitored folders."""
371
+ for monitored_folder in project_state.monitored_folders:
372
+ self.file_watcher.start_watching(monitored_folder.folder_path)
373
+
374
+ async def _add_subdirectories_to_monitored(self, project_state: ProjectState, parent_folder_path: str):
375
+ """Add all subdirectories of a folder to monitored_folders if not already present."""
376
+ try:
377
+ existing_paths = {mf.folder_path for mf in project_state.monitored_folders}
378
+ new_folders = []
379
+
380
+ with os.scandir(parent_folder_path) as entries:
381
+ for entry in entries:
382
+ if entry.is_dir() and entry.name != '.git' and not entry.name.startswith('.'):
383
+ if entry.path not in existing_paths:
384
+ new_monitored = MonitoredFolder(folder_path=entry.path, is_expanded=False)
385
+ project_state.monitored_folders.append(new_monitored)
386
+ new_folders.append(new_monitored)
387
+
388
+ # Start watching the new folders
389
+ for folder in new_folders:
390
+ self.file_watcher.start_watching(folder.folder_path)
391
+
392
+ except (OSError, PermissionError) as e:
393
+ logger.error("Error scanning folder %s for subdirectories: %s", parent_folder_path, e)
394
+
395
+ def _find_monitored_folder(self, project_state: ProjectState, folder_path: str) -> Optional[MonitoredFolder]:
396
+ """Find a monitored folder by path."""
397
+ for monitored_folder in project_state.monitored_folders:
398
+ if monitored_folder.folder_path == folder_path:
399
+ return monitored_folder
400
+ return None
401
+
338
402
  async def _load_directory_items(self, project_state: ProjectState, directory_path: str, is_root: bool = False, parent_item: Optional[FileItem] = None):
339
403
  """Load directory items with Git metadata."""
340
404
  git_manager = self.git_managers.get(project_state.project_id)
@@ -395,23 +459,35 @@ class ProjectStateManager:
395
459
  logger.error("Error loading directory %s: %s", directory_path, e)
396
460
 
397
461
  async def _build_flattened_items_structure(self, project_state: ProjectState):
398
- """Build a flattened items structure including all visible items and one level down from expanded folders."""
462
+ """Build a flattened items structure based on monitored folders and their expansion states."""
399
463
  all_items = []
400
464
 
465
+ # Create a set of expanded folder paths for quick lookup
466
+ expanded_paths = {mf.folder_path for mf in project_state.monitored_folders if mf.is_expanded}
467
+
401
468
  # Load root items
402
469
  root_items = await self._load_directory_items_list(project_state.project_folder_path, project_state.project_folder_path)
403
470
 
471
+ # Mark root directories as expanded if they are in expanded_paths
404
472
  for item in root_items:
473
+ if item.is_directory and item.path in expanded_paths:
474
+ item.is_expanded = True
405
475
  all_items.append(item)
406
-
407
- # Always load one level down from root folders (project root is always "expanded")
408
- # OR if this folder is explicitly expanded, add its children and one level down
409
- if item.is_directory and (item.parent_path == project_state.project_folder_path or item.is_expanded):
410
- children = await self._load_directory_items_list(item.path, item.path)
476
+
477
+ # For each expanded folder, load its contents and one level down from each subdirectory
478
+ for monitored_folder in project_state.monitored_folders:
479
+ if monitored_folder.is_expanded and monitored_folder.folder_path != project_state.project_folder_path:
480
+ # Load direct children of the expanded folder
481
+ children = await self._load_directory_items_list(monitored_folder.folder_path, monitored_folder.folder_path)
482
+
483
+ # Mark children directories as expanded if they are in expanded_paths
411
484
  for child in children:
485
+ if child.is_directory and child.path in expanded_paths:
486
+ child.is_expanded = True
412
487
  all_items.append(child)
413
-
414
- # If child is a directory, load one level down
488
+
489
+ # For each subdirectory in the expanded folder, load one level down (preview)
490
+ for child in children:
415
491
  if child.is_directory:
416
492
  grandchildren = await self._load_directory_items_list(child.path, child.path)
417
493
  all_items.extend(grandchildren)
@@ -444,12 +520,6 @@ class ProjectStateManager:
444
520
  if git_manager:
445
521
  git_info = git_manager.get_file_status(entry.path)
446
522
 
447
- # Check if this directory is expanded by finding it in current items
448
- is_expanded = False
449
- if entry.is_dir():
450
- # Check if this folder is expanded by looking for existing items with this path as parent
451
- is_expanded = self._is_folder_expanded(entry.path)
452
-
453
523
  file_item = FileItem(
454
524
  name=entry.name,
455
525
  path=entry.path,
@@ -461,7 +531,7 @@ class ProjectStateManager:
461
531
  git_status=git_info["status"],
462
532
  is_hidden=is_hidden,
463
533
  is_ignored=git_info["is_ignored"],
464
- is_expanded=is_expanded,
534
+ is_expanded=False,
465
535
  is_loaded=not entry.is_dir()
466
536
  )
467
537
 
@@ -479,16 +549,6 @@ class ProjectStateManager:
479
549
 
480
550
  return items
481
551
 
482
- def _is_folder_expanded(self, folder_path: str) -> bool:
483
- """Check if a folder is expanded by looking at existing items."""
484
- # During initial load, no folders are expanded
485
- # During updates, check if any items have this folder as parent_path
486
- for project_state in self.projects.values():
487
- for item in project_state.items:
488
- if item.parent_path == folder_path:
489
- return True
490
- return False
491
-
492
552
  async def expand_folder(self, project_id: str, folder_path: str) -> bool:
493
553
  """Expand a folder and load its contents."""
494
554
  if project_id not in self.projects:
@@ -503,8 +563,13 @@ class ProjectStateManager:
503
563
 
504
564
  folder_item.is_expanded = True
505
565
 
506
- # Start watching this folder
507
- self.file_watcher.start_watching(folder_path)
566
+ # Update the monitored folder to expanded state
567
+ monitored_folder = self._find_monitored_folder(project_state, folder_path)
568
+ if monitored_folder:
569
+ monitored_folder.is_expanded = True
570
+
571
+ # Add all subdirectories of the expanded folder to monitored folders
572
+ await self._add_subdirectories_to_monitored(project_state, folder_path)
508
573
 
509
574
  # Rebuild the entire flattened structure to include new expanded content
510
575
  await self._build_flattened_items_structure(project_state)
@@ -526,9 +591,13 @@ class ProjectStateManager:
526
591
 
527
592
  folder_item.is_expanded = False
528
593
 
529
- # Stop watching collapsed folders (except root)
530
- if folder_path != project_state.project_folder_path:
531
- self.file_watcher.stop_watching(folder_path)
594
+ # Update the monitored folder to collapsed state
595
+ monitored_folder = self._find_monitored_folder(project_state, folder_path)
596
+ if monitored_folder:
597
+ monitored_folder.is_expanded = False
598
+
599
+ # Note: We keep monitoring collapsed folders for file changes
600
+ # but don't stop watching them as we want to detect new files/folders
532
601
 
533
602
  # Rebuild the flattened structure to remove collapsed content
534
603
  await self._build_flattened_items_structure(project_state)
@@ -665,12 +734,9 @@ class ProjectStateManager:
665
734
  if project_id in self.projects:
666
735
  project_state = self.projects[project_id]
667
736
 
668
- # Stop watching all folders for this project
669
- self.file_watcher.stop_watching(project_state.project_folder_path)
670
- # Stop watching all expanded folders
671
- for item in project_state.items:
672
- if item.is_directory and item.is_expanded:
673
- self.file_watcher.stop_watching(item.path)
737
+ # Stop watching all monitored folders for this project
738
+ for monitored_folder in project_state.monitored_folders:
739
+ self.file_watcher.stop_watching(monitored_folder.folder_path)
674
740
 
675
741
  # Clean up managers
676
742
  self.git_managers.pop(project_id, None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.16.dev7
3
+ Version: 0.3.16.dev9
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