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
This commit is contained in:
Robert Brennan 2024-09-11 09:44:37 -04:00 committed by GitHub
parent 93f271579c
commit c6105f264f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 42 additions and 51 deletions

View File

@ -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]">

View File

@ -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();

View File

@ -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": "工作区刷新错误",

View File

@ -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]

View File

@ -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]}'
)

View File

@ -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