diff --git a/frontend/src/components/file-explorer/ExplorerTree.tsx b/frontend/src/components/file-explorer/ExplorerTree.tsx index 32b014d9a6..9d1033cc99 100644 --- a/frontend/src/components/file-explorer/ExplorerTree.tsx +++ b/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 ( -
- {t(I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE)} -
- ); + if (!files?.length) { + const message = !files + ? I18nKey.EXPLORER$LOADING_WORKSPACE_MESSAGE + : I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE; + return
{t(message)}
; } return (
diff --git a/frontend/src/components/file-explorer/FileExplorer.tsx b/frontend/src/components/file-explorer/FileExplorer.tsx index 7691dc143e..f2cf035022 100644 --- a/frontend/src/components/file-explorer/FileExplorer.tsx +++ b/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([]); + const [files, setFiles] = React.useState(null); const { curAgentState } = useSelector((state: RootState) => state.agent); const fileInputRef = React.useRef(null); const dispatch = useDispatch(); diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index ba379af957..be4558273c 100644 --- a/frontend/src/i18n/translation.json +++ b/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": "工作区刷新错误", diff --git a/openhands/runtime/client/client.py b/openhands/runtime/client/client.py index c4d547f2bb..96df63f614 100644 --- a/openhands/runtime/client/client.py +++ b/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] diff --git a/openhands/runtime/client/runtime.py b/openhands/runtime/client/runtime.py index 6b91fceda7..5767632f37 100644 --- a/openhands/runtime/client/runtime.py +++ b/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]}' ) diff --git a/openhands/server/listen.py b/openhands/server/listen.py index 2b52390358..680aad7afe 100644 --- a/openhands/server/listen.py +++ b/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