Explorar o código

Improvements to file list UI (#3794)

* move filematching logic into server

* wait until ready before returning

* show loading message instead of empty

* logspam

* delint

* fix type

* add a few more default ignores
Robert Brennan hai 1 ano
pai
achega
c6105f264f

+ 6 - 7
frontend/src/components/file-explorer/ExplorerTree.tsx

@@ -4,18 +4,17 @@ import TreeNode from "./TreeNode";
 import { I18nKey } from "#/i18n/declaration";
 
 interface ExplorerTreeProps {
-  files: string[];
+  files: string[] | null;
   defaultOpen?: boolean;
 }
 
 function ExplorerTree({ files, defaultOpen = false }: ExplorerTreeProps) {
   const { t } = useTranslation();
-  if (files.length === 0) {
-    return (
-      <div className="text-sm text-gray-400 pt-4">
-        {t(I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE)}
-      </div>
-    );
+  if (!files?.length) {
+    const message = !files
+      ? I18nKey.EXPLORER$LOADING_WORKSPACE_MESSAGE
+      : I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE;
+    return <div className="text-sm text-gray-400 pt-4">{t(message)}</div>;
   }
   return (
     <div className="w-full h-full pt-[4px]">

+ 1 - 1
frontend/src/components/file-explorer/FileExplorer.tsx

@@ -90,7 +90,7 @@ function ExplorerActions({
 function FileExplorer() {
   const [isHidden, setIsHidden] = React.useState(false);
   const [isDragging, setIsDragging] = React.useState(false);
-  const [files, setFiles] = React.useState<string[]>([]);
+  const [files, setFiles] = React.useState<string[] | null>(null);
   const { curAgentState } = useSelector((state: RootState) => state.agent);
   const fileInputRef = React.useRef<HTMLInputElement | null>(null);
   const dispatch = useDispatch();

+ 5 - 0
frontend/src/i18n/translation.json

@@ -441,6 +441,11 @@
     "zh-CN": "工作区没有文件",
     "de": "Keine Dateien im Arbeitsbereich"
   },
+  "EXPLORER$LOADING_WORKSPACE_MESSAGE": {
+    "en": "Loading workspace...",
+    "zh-CN": "正在加载工作区...",
+    "de": "Arbeitsbereich wird geladen..."
+  },
   "EXPLORER$REFRESH_ERROR_MESSAGE": {
     "en": "Error refreshing workspace",
     "zh-CN": "工作区刷新错误",

+ 1 - 43
openhands/runtime/client/client.py

@@ -17,8 +17,6 @@ from pathlib import Path
 import pexpect
 from fastapi import FastAPI, HTTPException, Request, UploadFile
 from fastapi.responses import JSONResponse
-from pathspec import PathSpec
-from pathspec.patterns import GitWildMatchPattern
 from pydantic import BaseModel
 from uvicorn import run
 
@@ -649,52 +647,12 @@ if __name__ == '__main__':
             if not os.path.exists(full_path) or not os.path.isdir(full_path):
                 return []
 
-            # Check if .gitignore exists
-            gitignore_path = os.path.join(full_path, '.gitignore')
-            if os.path.exists(gitignore_path):
-                # Use PathSpec to parse .gitignore
-                with open(gitignore_path, 'r') as f:
-                    spec = PathSpec.from_lines(GitWildMatchPattern, f.readlines())
-            else:
-                # Fallback to default exclude list if .gitignore doesn't exist
-                default_exclude = [
-                    '.git',
-                    '.DS_Store',
-                    '.svn',
-                    '.hg',
-                    '.idea',
-                    '.vscode',
-                    '.settings',
-                    '.pytest_cache',
-                    '__pycache__',
-                    'node_modules',
-                    'vendor',
-                    'build',
-                    'dist',
-                    'bin',
-                    'logs',
-                    'log',
-                    'tmp',
-                    'temp',
-                    'coverage',
-                    'venv',
-                    'env',
-                ]
-                spec = PathSpec.from_lines(GitWildMatchPattern, default_exclude)
-
             entries = os.listdir(full_path)
 
-            # Filter entries using PathSpec
-            filtered_entries = [
-                os.path.join(full_path, entry)
-                for entry in entries
-                if not spec.match_file(os.path.relpath(entry, str(full_path)))
-            ]
-
             # Separate directories and files
             directories = []
             files = []
-            for entry in filtered_entries:
+            for entry in entries:
                 # Remove leading slash and any parent directory components
                 entry_relative = entry.lstrip('/').split('/')[-1]
 

+ 2 - 0
openhands/runtime/client/runtime.py

@@ -159,6 +159,8 @@ class EventStreamRuntime(Runtime):
         # will initialize both the event stream and the env vars
         super().__init__(config, event_stream, sid, plugins, env_vars)
 
+        self._wait_until_alive()
+
         logger.info(
             f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}'
         )

+ 27 - 0
openhands/server/listen.py

@@ -5,6 +5,8 @@ import uuid
 import warnings
 
 import requests
+from pathspec import PathSpec
+from pathspec.patterns import GitWildMatchPattern
 
 from openhands.security.options import SecurityAnalyzers
 from openhands.server.data_models.feedback import FeedbackDataModel, store_feedback
@@ -378,6 +380,14 @@ async def get_security_analyzers():
     return sorted(SecurityAnalyzers.keys())
 
 
+FILES_TO_IGNORE = [
+    '.git/',
+    '.DS_Store',
+    'node_modules/',
+    '__pycache__/',
+]
+
+
 @app.get('/api/list-files')
 async def list_files(request: Request, path: str | None = None):
     """List files in the specified path.
@@ -407,6 +417,23 @@ async def list_files(request: Request, path: str | None = None):
         )
     runtime: Runtime = request.state.session.agent_session.runtime
     file_list = runtime.list_files(path)
+    file_list = [f for f in file_list if f not in FILES_TO_IGNORE]
+
+    def filter_for_gitignore(file_list, base_path):
+        gitignore_path = os.path.join(base_path, '.gitignore')
+        try:
+            read_action = FileReadAction(gitignore_path)
+            observation = runtime.run_action(read_action)
+            spec = PathSpec.from_lines(
+                GitWildMatchPattern, observation.content.splitlines()
+            )
+        except Exception as e:
+            print(e)
+            return file_list
+        file_list = [entry for entry in file_list if not spec.match_file(entry)]
+        return file_list
+
+    file_list = filter_for_gitignore(file_list, '')
     return file_list