diff --git a/agent-node/agent_node/node.py b/agent-node/agent_node/node.py index cbb14fd..dc2446c 100644 --- a/agent-node/agent_node/node.py +++ b/agent-node/agent_node/node.py @@ -224,9 +224,9 @@ """Pushes the current local manifest back to the server.""" print(f" [📁📤] Pushing {'Shallow' if shallow else 'Full'} Manifest for {session_id}") - # M6: If __fs_explorer__, we are browsing the root sync dir, otherwise session-scoped + # M6: If __fs_explorer__, we are browsing the root filesystem (terminal-like), otherwise session-scoped if session_id == "__fs_explorer__": - base_dir = self.sync_mgr.base_sync_dir + base_dir = os.getenv("CORTEX_FS_ROOT", "/") else: base_dir = self.sync_mgr.get_session_dir(session_id) @@ -293,7 +293,7 @@ """Modular FS Write/Create.""" try: if session_id == "__fs_explorer__": - base_dir = self.sync_mgr.base_sync_dir + base_dir = os.getenv("CORTEX_FS_ROOT", "/") else: base_dir = self.sync_mgr.get_session_dir(session_id) @@ -331,7 +331,7 @@ """Modular FS Delete.""" try: if session_id == "__fs_explorer__": - base_dir = self.sync_mgr.base_sync_dir + base_dir = os.getenv("CORTEX_FS_ROOT", "/") else: base_dir = self.sync_mgr.get_session_dir(session_id) @@ -370,7 +370,7 @@ def _push_file(self, session_id, rel_path, task_id=""): """Pushes a specific file from node to server.""" if session_id == "__fs_explorer__": - watch_path = self.sync_mgr.base_sync_dir + watch_path = os.getenv("CORTEX_FS_ROOT", "/") else: watch_path = self.watcher.get_watch_path(session_id) if not watch_path: diff --git a/ai-hub/app/core/grpc/services/assistant.py b/ai-hub/app/core/grpc/services/assistant.py index 55f6ab8..409d88a 100644 --- a/ai-hub/app/core/grpc/services/assistant.py +++ b/ai-hub/app/core/grpc/services/assistant.py @@ -173,10 +173,23 @@ if event.wait(timeout): res = self.journal.get_result(tid) self.journal.pop(tid) + + # Proactive Mirroring for Explorer: start fetching content so dots turn green + if res and "files" in res: + self._proactive_explorer_sync(node_id, res["files"]) + return res self.journal.pop(tid) return {"error": "Timeout"} + def _proactive_explorer_sync(self, node_id, files): + """Starts background tasks to mirror files to Hub so dots turn green.""" + import threading + for f in files: + if f.get("is_dir"): continue + if not f.get("is_synced") and f.get("size", 0) < 1024 * 512: # Skip large files + threading.Thread(target=self.cat, args=(node_id, f["path"]), daemon=True).start() + def cat(self, node_id: str, path: str, timeout=15): """Requests file content from a node (waits for result).""" node = self.registry.get_node(node_id) @@ -198,6 +211,7 @@ if event.wait(timeout): res = self.journal.get_result(tid) self.journal.pop(tid) + # res usually contains {content, path}. grpc_server already writes it to mirror. return res self.journal.pop(tid) return {"error": "Timeout"} @@ -226,6 +240,18 @@ if event.wait(timeout): res = self.journal.get_result(tid) self.journal.pop(tid) + + # M6: Update mirror locally on hub so ls sees it as synced + if self.mirror and res.get("status") == "OK": + workspace_mirror = self.mirror.get_workspace_path("__fs_explorer__") + dest = os.path.join(workspace_mirror, path) + if is_dir: + os.makedirs(dest, exist_ok=True) + else: + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "wb") as f: + f.write(content) + return res self.journal.pop(tid) return {"error": "Timeout"} @@ -249,6 +275,14 @@ if event.wait(timeout): res = self.journal.get_result(tid) self.journal.pop(tid) + + # M6: remove from mirror if successful + if self.mirror and res.get("status") == "OK": + import shutil + dest = os.path.join(self.mirror.get_workspace_path("__fs_explorer__"), path) + if os.path.isdir(dest): shutil.rmtree(dest) + elif os.path.exists(dest): os.remove(dest) + return res self.journal.pop(tid) return {"error": "Timeout"} diff --git a/ui/client-app/src/components/FileSystemNavigator.js b/ui/client-app/src/components/FileSystemNavigator.js index 7de06e2..5cf8f65 100644 --- a/ui/client-app/src/components/FileSystemNavigator.js +++ b/ui/client-app/src/components/FileSystemNavigator.js @@ -35,8 +35,18 @@ }, [initialPath, fetchLevel]); useEffect(() => { - if (nodeId) loadRoot(); - }, [nodeId, loadRoot]); + if (nodeId) { + loadRoot(); + // Polling for sync status if there are unsynced files + const interval = setInterval(() => { + const hasUnsynced = tree.some(f => !f.is_dir && !f.is_synced); + if (hasUnsynced || tree.length === 0) { + loadRoot(); + } + }, 8000); + return () => clearInterval(interval); + } + }, [nodeId, loadRoot, tree]); const toggleFolder = async (path) => { const isExpanded = expanded[path];