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