portacode 1.4.15.dev26__py3-none-any.whl → 1.4.16.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.
portacode/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.4.15.dev26'
32
- __version_tuple__ = version_tuple = (1, 4, 15, 'dev26')
31
+ __version__ = version = '1.4.16.dev0'
32
+ __version_tuple__ = version_tuple = (1, 4, 16, 'dev0')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -21,6 +21,24 @@ from .git_manager import GitManager
21
21
  from .file_system_watcher import FileSystemWatcher
22
22
  from ....logging_categories import get_categorized_logger, LogCategory
23
23
 
24
+ def _deterministic_file_tab_id(file_path: str) -> str:
25
+ try:
26
+ resolved = os.path.abspath(file_path)
27
+ mtime = int(Path(resolved).stat().st_mtime)
28
+ except OSError:
29
+ mtime = "unknown"
30
+ resolved = os.path.abspath(file_path)
31
+ return f"{resolved}:{mtime}"
32
+
33
+ def _deterministic_diff_tab_id(
34
+ file_path: str,
35
+ from_ref: str,
36
+ to_ref: str,
37
+ from_hash: Optional[str],
38
+ to_hash: Optional[str]
39
+ ) -> str:
40
+ return f"diff:{os.path.abspath(file_path)}:{from_ref}:{to_ref}:{from_hash or ''}:{to_hash or ''}"
41
+
24
42
  logger = get_categorized_logger(__name__)
25
43
 
26
44
  # Global singleton instance
@@ -657,10 +675,10 @@ class ProjectStateManager:
657
675
  # Create new file tab using tab factory
658
676
  from ..tab_factory import get_tab_factory
659
677
  tab_factory = get_tab_factory()
660
-
678
+
661
679
  try:
662
680
  logger.info(f"About to create tab for file: {file_path}")
663
- new_tab = await tab_factory.create_file_tab(file_path)
681
+ new_tab = await tab_factory.create_file_tab(file_path, tab_id=_deterministic_file_tab_id(file_path))
664
682
  logger.info(f"Tab created successfully, adding to project state")
665
683
  project_state.open_tabs[tab_key] = new_tab
666
684
  if set_active:
@@ -842,7 +860,11 @@ class ProjectStateManager:
842
860
  diff_title = f"{os.path.basename(file_path)} ({' '.join(title_parts)})"
843
861
 
844
862
  diff_tab = await tab_factory.create_diff_tab_with_title(
845
- file_path, original_content, modified_content, diff_title,
863
+ file_path,
864
+ original_content,
865
+ modified_content,
866
+ diff_title,
867
+ tab_id=_deterministic_diff_tab_id(file_path, from_ref, to_ref, from_hash, to_hash),
846
868
  diff_details=diff_details
847
869
  )
848
870
 
@@ -410,6 +410,7 @@ def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str
410
410
  containers.append(
411
411
  {
412
412
  "vmid": str(_as_int(record.get("vmid"))) if record.get("vmid") is not None else None,
413
+ "device_id": record.get("device_id"),
413
414
  "hostname": record.get("hostname"),
414
415
  "template": record.get("template"),
415
416
  "storage": record.get("storage"),
@@ -876,12 +877,17 @@ def _parse_ctid(message: Dict[str, Any]) -> int:
876
877
 
877
878
 
878
879
  def _ensure_container_managed(
879
- proxmox: Any, node: str, vmid: int
880
+ proxmox: Any, node: str, vmid: int, *, device_id: Optional[str] = None
880
881
  ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
881
882
  record = _read_container_record(vmid)
882
883
  ct_cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
883
884
  if not ct_cfg or MANAGED_MARKER not in (ct_cfg.get("description") or ""):
884
885
  raise RuntimeError(f"Container {vmid} is not managed by Portacode.")
886
+ record_device_id = record.get("device_id")
887
+ if device_id and str(record_device_id or "") != str(device_id):
888
+ raise RuntimeError(
889
+ f"Container {vmid} is managed for device {record_device_id!r}, not {device_id!r}."
890
+ )
885
891
  return record, ct_cfg
886
892
 
887
893
 
@@ -1835,10 +1841,13 @@ class StartProxmoxContainerHandler(SyncHandler):
1835
1841
 
1836
1842
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1837
1843
  vmid = _parse_ctid(message)
1844
+ on_behalf_of_device = (message.get("on_behalf_of_device") or "").strip()
1845
+ if not on_behalf_of_device:
1846
+ raise ValueError("on_behalf_of_device is required for start_proxmox_container")
1838
1847
  config = _ensure_infra_configured()
1839
1848
  proxmox = _connect_proxmox(config)
1840
1849
  node = _get_node_from_config(config)
1841
- _ensure_container_managed(proxmox, node, vmid)
1850
+ _ensure_container_managed(proxmox, node, vmid, device_id=on_behalf_of_device)
1842
1851
 
1843
1852
  status, elapsed = _start_container(proxmox, node, vmid)
1844
1853
  _update_container_record(vmid, {"status": "running"})
@@ -1853,6 +1862,7 @@ class StartProxmoxContainerHandler(SyncHandler):
1853
1862
  "details": {"exitstatus": status.get("exitstatus")},
1854
1863
  "status": status.get("status"),
1855
1864
  "infra": infra,
1865
+ "on_behalf_of_device": on_behalf_of_device,
1856
1866
  }
1857
1867
 
1858
1868
 
@@ -1865,10 +1875,13 @@ class StopProxmoxContainerHandler(SyncHandler):
1865
1875
 
1866
1876
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1867
1877
  vmid = _parse_ctid(message)
1878
+ on_behalf_of_device = (message.get("on_behalf_of_device") or "").strip()
1879
+ if not on_behalf_of_device:
1880
+ raise ValueError("on_behalf_of_device is required for stop_proxmox_container")
1868
1881
  config = _ensure_infra_configured()
1869
1882
  proxmox = _connect_proxmox(config)
1870
1883
  node = _get_node_from_config(config)
1871
- _ensure_container_managed(proxmox, node, vmid)
1884
+ _ensure_container_managed(proxmox, node, vmid, device_id=on_behalf_of_device)
1872
1885
 
1873
1886
  status, elapsed = _stop_container(proxmox, node, vmid)
1874
1887
  final_status = status.get("status") or "stopped"
@@ -1889,6 +1902,7 @@ class StopProxmoxContainerHandler(SyncHandler):
1889
1902
  "details": {"exitstatus": status.get("exitstatus")},
1890
1903
  "status": final_status,
1891
1904
  "infra": infra,
1905
+ "on_behalf_of_device": on_behalf_of_device,
1892
1906
  }
1893
1907
 
1894
1908
 
@@ -1901,10 +1915,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1901
1915
 
1902
1916
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1903
1917
  vmid = _parse_ctid(message)
1918
+ on_behalf_of_device = (message.get("on_behalf_of_device") or "").strip()
1919
+ if not on_behalf_of_device:
1920
+ raise ValueError("on_behalf_of_device is required for remove_proxmox_container")
1904
1921
  config = _ensure_infra_configured()
1905
1922
  proxmox = _connect_proxmox(config)
1906
1923
  node = _get_node_from_config(config)
1907
- _ensure_container_managed(proxmox, node, vmid)
1924
+ _ensure_container_managed(proxmox, node, vmid, device_id=on_behalf_of_device)
1908
1925
 
1909
1926
  stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
1910
1927
  delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
@@ -1916,6 +1933,7 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1916
1933
  "action": "remove",
1917
1934
  "success": True,
1918
1935
  "ctid": str(vmid),
1936
+ "on_behalf_of_device": on_behalf_of_device,
1919
1937
  "message": f"Deleted container {vmid} in {delete_elapsed:.1f}s.",
1920
1938
  "details": {
1921
1939
  "stop_exitstatus": stop_status.get("exitstatus"),
@@ -685,6 +685,7 @@ class TerminalManager:
685
685
  try:
686
686
  # Initialize project state
687
687
  project_state = await manager.initialize_project_state(session_name, project_folder_path)
688
+ await self._restore_tabs_from_session_metadata(manager, project_state, session)
688
689
 
689
690
  # Send initial project state to the client
690
691
  initial_state_payload = {
@@ -710,6 +711,85 @@ class TerminalManager:
710
711
 
711
712
  except Exception as exc:
712
713
  logger.exception("terminal_manager: Error initializing project states for new sessions: %s", exc)
714
+
715
+ async def _restore_tabs_from_session_metadata(self, manager, project_state, session):
716
+ """Restore open tabs/active tab from client session metadata if available."""
717
+ if not session or not project_state:
718
+ return
719
+
720
+ descriptors = session.get("open_tabs") or []
721
+ if not descriptors:
722
+ return
723
+
724
+ session_id = project_state.client_session_id
725
+ logger.info("terminal_manager: 🧭 Restoring %d tabs from metadata for session %s", len(descriptors), session_id)
726
+
727
+ for descriptor in descriptors:
728
+ parsed = self._parse_tab_descriptor(descriptor)
729
+ if not parsed:
730
+ continue
731
+
732
+ tab_type = parsed.get("tab_type")
733
+ file_path = parsed.get("file_path")
734
+ metadata = parsed.get("metadata", {})
735
+
736
+ if tab_type == "file" and file_path:
737
+ try:
738
+ await manager.open_file(session_id, file_path, set_active=False)
739
+ except Exception as exc:
740
+ logger.warning("terminal_manager: Failed to restore file tab %s for session %s: %s", file_path, session_id, exc)
741
+ continue
742
+
743
+ if tab_type == "diff" and file_path:
744
+ from_ref = metadata.get("from") or metadata.get("from_ref")
745
+ to_ref = metadata.get("to") or metadata.get("to_ref")
746
+ if not from_ref or not to_ref:
747
+ logger.warning("terminal_manager: Skipping diff tab %s for session %s because from/to references are missing", file_path, session_id)
748
+ continue
749
+ from_hash = metadata.get("from_hash") or metadata.get("fromHash")
750
+ to_hash = metadata.get("to_hash") or metadata.get("toHash")
751
+ try:
752
+ await manager.open_diff_tab(session_id, file_path, from_ref, to_ref, from_hash=from_hash, to_hash=to_hash)
753
+ except Exception as exc:
754
+ logger.warning("terminal_manager: Failed to restore diff tab %s for session %s: %s", file_path, session_id, exc)
755
+ continue
756
+
757
+ logger.debug("terminal_manager: Unknown tab descriptor ignored for session %s: %s", session_id, descriptor)
758
+
759
+ active_index = session.get("active_tab")
760
+ try:
761
+ active_index_int = int(active_index) if active_index is not None else None
762
+ except (TypeError, ValueError):
763
+ active_index_int = None
764
+
765
+ if active_index_int is not None and active_index_int >= 0:
766
+ current_tabs = list(project_state.open_tabs.values())
767
+ if 0 <= active_index_int < len(current_tabs):
768
+ try:
769
+ await manager.set_active_tab(session_id, current_tabs[active_index_int].tab_id)
770
+ except Exception as exc:
771
+ logger.warning("terminal_manager: Failed to set active tab for session %s: %s", session_id, exc)
772
+ else:
773
+ logger.debug("terminal_manager: Active tab index %s out of range for session %s", active_index_int, session_id)
774
+
775
+ def _parse_tab_descriptor(self, descriptor: str) -> Optional[Dict[str, Any]]:
776
+ """Parse a URL-friendly tab descriptor string."""
777
+ if not descriptor:
778
+ return None
779
+
780
+ try:
781
+ parts = descriptor.split("|")
782
+ tab_type = parts[0] if parts else None
783
+ file_path = parts[1] if len(parts) > 1 else None
784
+ metadata = {}
785
+ for part in parts[2:]:
786
+ if "=" in part:
787
+ key, value = part.split("=", 1)
788
+ metadata[key] = value
789
+ return {"tab_type": tab_type, "file_path": file_path, "metadata": metadata}
790
+ except Exception as exc:
791
+ logger.warning("terminal_manager: Failed to parse tab descriptor '%s': %s", descriptor, exc)
792
+ return None
713
793
 
714
794
  async def _send_targeted_terminal_list(self, message: Dict[str, Any], target_sessions: List[str]) -> None:
715
795
  """Send terminal_list command to specific client sessions.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.15.dev26
3
+ Version: 1.4.16.dev0
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -1,7 +1,7 @@
1
1
  portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
2
2
  portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
3
3
  portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
4
- portacode/_version.py,sha256=NaDQN6JFm1U_l_JaPePDvZOoOJh-wWOSLTPt8VTkIao,721
4
+ portacode/_version.py,sha256=VRcygEdkbj_kT_PkjvrdxuvM8kPIsg1rBzzADtCIQiA,719
5
5
  portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
6
6
  portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
7
7
  portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
@@ -12,7 +12,7 @@ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJ
12
12
  portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
13
13
  portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5weiU,14181
14
14
  portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
15
- portacode/connection/terminal.py,sha256=oyLPOVLPlUuN_eRvHPGazB51yi8W8JEF3oOEYxucGTE,45069
15
+ portacode/connection/terminal.py,sha256=n1Uu92JacV5K6d1Qwx94Tw9OB2Tpke5HqsW2NDn76Ls,49032
16
16
  portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
17
17
  portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=OtObmoVAXlf0uU1HidTWNmyJYBS1Yl6Rpgyh6TOTjUQ,100590
18
18
  portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
@@ -22,7 +22,7 @@ portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZb
22
22
  portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
23
23
  portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
24
24
  portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
25
- portacode/connection/handlers/proxmox_infra.py,sha256=DE2TUPKwW4bZ9L7vxyE_m74-ra0JR9DfUCkUOPg5tpk,71908
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=Pe-GX5IjQhjyBPxm0hNNLoKm7NZPmOsRCPRdEIkmjhE,73134
26
26
  portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
27
27
  portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
28
28
  portacode/connection/handlers/system_handlers.py,sha256=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
@@ -35,7 +35,7 @@ portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL
35
35
  portacode/connection/handlers/project_state/file_system_watcher.py,sha256=r9_UKxWTbzum0jGqxIafe68Ced2Y3xOp3ZmkpBOfRpw,8573
36
36
  portacode/connection/handlers/project_state/git_manager.py,sha256=iGQ7LYIA7uHsZHdj3HEc_LYo7S1Lqv6-AeyyMwknBPo,70027
37
37
  portacode/connection/handlers/project_state/handlers.py,sha256=qgOSt26rxAGNxW07AoevTwDPBdxblX4J-dX-EjOKtg4,38232
38
- portacode/connection/handlers/project_state/manager.py,sha256=pRMZqPOTK9YE3abNxiAbnERIJmRys673HFOEIBiKnm4,67184
38
+ portacode/connection/handlers/project_state/manager.py,sha256=ori_QpeoY1sdpY8WDYIx-kl_gNfZ7o8eq84CcOBlvIs,67915
39
39
  portacode/connection/handlers/project_state/models.py,sha256=EZTKvxHKs8QlQUbzI0u2IqfzfRRXZixUIDBwTGCJATI,4313
40
40
  portacode/connection/handlers/project_state/utils.py,sha256=LsbQr9TH9Bz30FqikmtTxco4PlB_n0kUIuPKQ6Fb_mo,1665
41
41
  portacode/link_capture/__init__.py,sha256=93LjyYDqzOimsIDBhsPibTl7tr-8DiIzyDF7JWQkE2A,1231
@@ -65,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
65
65
  portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
66
66
  portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
67
67
  portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
68
- portacode-1.4.15.dev26.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.16.dev0.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
69
69
  test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
70
70
  test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
71
71
  test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
@@ -91,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
91
91
  testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
92
92
  testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
93
93
  testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
94
- portacode-1.4.15.dev26.dist-info/METADATA,sha256=gRy_iG4GTwJV70SySeGLIVN6ipzliF3vug8YGIEFW8Q,13052
95
- portacode-1.4.15.dev26.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.15.dev26.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.15.dev26.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.15.dev26.dist-info/RECORD,,
94
+ portacode-1.4.16.dev0.dist-info/METADATA,sha256=UbwKMn3EpfoXPgYPALM8B5GwR0TluW6buRsbfQZhvlc,13051
95
+ portacode-1.4.16.dev0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.16.dev0.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.16.dev0.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.16.dev0.dist-info/RECORD,,