From 003ebc0dedac57f7e2d5767039d5cb69b29dd174 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Wed, 19 Feb 2025 12:54:34 -0500 Subject: [PATCH 01/41] feat: better error logging for remote runtime (#6805) --- openhands/runtime/impl/remote/remote_runtime.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index 70e4217e4718..7e1256d28303 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -153,6 +153,12 @@ def _check_existing_runtime(self) -> bool: return False self.log('debug', f'Error while looking for remote runtime: {e}') raise + except requests.exceptions.JSONDecodeError as e: + self.log( + 'error', + f'Invalid JSON response from runtime API: {e}. URL: {self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}. Response: {response}', + ) + raise if status == 'running': return True From b95840db0cae321e36a1434a6ab87fc92a9b6da1 Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Wed, 19 Feb 2025 19:24:35 +0100 Subject: [PATCH 02/41] hotfix azure (#6806) --- openhands/core/config/llm_config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openhands/core/config/llm_config.py b/openhands/core/config/llm_config.py index cee22766df14..5497d7125823 100644 --- a/openhands/core/config/llm_config.py +++ b/openhands/core/config/llm_config.py @@ -102,3 +102,9 @@ def model_post_init(self, __context: Any): os.environ['OR_SITE_URL'] = self.openrouter_site_url if self.openrouter_app_name: os.environ['OR_APP_NAME'] = self.openrouter_app_name + + # Assign an API version for Azure models + # While it doesn't seem required, the format supported by the API without version seems old and will likely break. + # Azure issue: https://github.com/All-Hands-AI/OpenHands/issues/6777 + if self.model.startswith('azure') and self.api_version is None: + self.api_version = '2024-08-01-preview' From 61ce6734004146a029ee232d8ab87c8b891adc3a Mon Sep 17 00:00:00 2001 From: mamoodi Date: Wed, 19 Feb 2025 14:30:05 -0500 Subject: [PATCH 03/41] Release 0.25.0 (#6782) --- Development.md | 2 +- README.md | 6 +++--- containers/dev/compose.yml | 2 +- docker-compose.yml | 2 +- .../current/usage/how-to/cli-mode.md | 4 ++-- .../current/usage/how-to/headless-mode.md | 4 ++-- .../current/usage/installation.mdx | 6 +++--- .../current/usage/runtimes.md | 2 +- .../current/usage/how-to/cli-mode.md | 4 ++-- .../current/usage/how-to/headless-mode.md | 4 ++-- .../current/usage/installation.mdx | 6 +++--- .../current/usage/runtimes.md | 2 +- docs/modules/usage/how-to/cli-mode.md | 4 ++-- docs/modules/usage/how-to/headless-mode.md | 4 ++-- docs/modules/usage/installation.mdx | 6 +++--- docs/modules/usage/runtimes.md | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- pyproject.toml | 4 +--- 19 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Development.md b/Development.md index 8d6e35751879..06bf415a7921 100644 --- a/Development.md +++ b/Development.md @@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image. -Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.24-nikolaik` +Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.25-nikolaik` ## Develop inside Docker container diff --git a/README.md b/README.md index 22caad34c99f..51cf52943e94 100644 --- a/README.md +++ b/README.md @@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio system requirements and more information. ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.openhands-state:/.openhands-state \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)! diff --git a/containers/dev/compose.yml b/containers/dev/compose.yml index 50c8ed04563f..34245f04efaf 100644 --- a/containers/dev/compose.yml +++ b/containers/dev/compose.yml @@ -11,7 +11,7 @@ services: - BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"} - SANDBOX_API_HOSTNAME=host.docker.internal # - - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.24-nikolaik} + - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.25-nikolaik} - SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace} ports: diff --git a/docker-compose.yml b/docker-compose.yml index 4353b7b6bb5b..6407f78b4030 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: image: openhands:latest container_name: openhands-app-${DATE:-} environment: - - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik} + - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik} #- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace} ports: diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md index 6a666e91f8d3..c00beec86d72 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md @@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -61,7 +61,7 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.cli ``` diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md index a72cd57f0cc1..1151e1e60df8 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md @@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -56,6 +56,6 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue ``` diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx index 6a1789214923..79a9bf0acdb7 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx @@ -13,16 +13,16 @@ La façon la plus simple d'exécuter OpenHands est avec Docker. ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action). diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md index 865489d34841..d256032b4c4c 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md @@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands. ``` docker run # ... - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -v /var/run/docker.sock:/var/run/docker.sock \ # ... ``` diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md index 57b95b719570..4aafa581294b 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md @@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -59,7 +59,7 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.cli ``` diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md index 44a4b5bc6f63..a164f3a9ba8b 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md @@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -57,6 +57,6 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue ``` diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx index 2d20773af4bc..4988d8d4c7da 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx @@ -11,16 +11,16 @@ 在 Docker 中运行 OpenHands 是最简单的方式。 ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` 你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。 diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md index 5786ce571c81..e2d4bde47a2e 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md @@ -11,7 +11,7 @@ ``` docker run # ... - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -v /var/run/docker.sock:/var/run/docker.sock \ # ... ``` diff --git a/docs/modules/usage/how-to/cli-mode.md b/docs/modules/usage/how-to/cli-mode.md index 612f1590eac9..630f63b9697a 100644 --- a/docs/modules/usage/how-to/cli-mode.md +++ b/docs/modules/usage/how-to/cli-mode.md @@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker: ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -45,7 +45,7 @@ docker run -it \ -v ~/.openhands-state:/.openhands-state \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.cli ``` diff --git a/docs/modules/usage/how-to/headless-mode.md b/docs/modules/usage/how-to/headless-mode.md index b751dc3000d1..e68b1e494934 100644 --- a/docs/modules/usage/how-to/headless-mode.md +++ b/docs/modules/usage/how-to/headless-mode.md @@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker: ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -43,7 +43,7 @@ docker run -it \ -v ~/.openhands-state:/.openhands-state \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.main -t "write a bash script that prints hi" ``` diff --git a/docs/modules/usage/installation.mdx b/docs/modules/usage/installation.mdx index 72d5300f3f03..64a6014e7d8d 100644 --- a/docs/modules/usage/installation.mdx +++ b/docs/modules/usage/installation.mdx @@ -56,17 +56,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to The easiest way to run OpenHands is in Docker. ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.openhands-state:/.openhands-state \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` You'll find OpenHands running at http://localhost:3000! diff --git a/docs/modules/usage/runtimes.md b/docs/modules/usage/runtimes.md index 740a53b00482..1fb2d0f4236d 100644 --- a/docs/modules/usage/runtimes.md +++ b/docs/modules/usage/runtimes.md @@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible: ``` docker run # ... - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -v /var/run/docker.sock:/var/run/docker.sock \ # ... ``` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f03e27c271f6..12eb28118046 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "openhands-frontend", - "version": "0.24.0", + "version": "0.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openhands-frontend", - "version": "0.24.0", + "version": "0.25.0", "dependencies": { "@heroui/react": "2.6.14", "@monaco-editor/react": "^4.7.0-rc.0", diff --git a/frontend/package.json b/frontend/package.json index b840a3e51418..4fc89d93629d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "openhands-frontend", - "version": "0.24.0", + "version": "0.25.0", "private": true, "type": "module", "engines": { diff --git a/pyproject.toml b/pyproject.toml index d8e474d097f4..b11d2e8991b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openhands-ai" -version = "0.24.0" +version = "0.25.0" description = "OpenHands: Code Less, Make More" authors = ["OpenHands"] license = "MIT" @@ -108,7 +108,6 @@ reportlab = "*" [tool.coverage.run] concurrency = ["gevent"] - [tool.poetry.group.runtime.dependencies] jupyterlab = "*" notebook = "*" @@ -137,7 +136,6 @@ ignore = ["D1"] [tool.ruff.lint.pydocstyle] convention = "google" - [tool.poetry.group.evaluation.dependencies] streamlit = "*" whatthepatch = "*" From e92e4a1cbc8b3788c35266b5a73599847e922414 Mon Sep 17 00:00:00 2001 From: mamoodi Date: Wed, 19 Feb 2025 14:30:36 -0500 Subject: [PATCH 04/41] Update documentation with new settings page (#6716) Co-authored-by: Engel Nyst --- docs/DOC_STYLE_GUIDE.md | 8 ++ docs/modules/usage/how-to/gui-mode.md | 124 ++++++++++------------- docs/modules/usage/installation.mdx | 30 +++--- docs/modules/usage/llms/azure-llms.md | 2 +- docs/modules/usage/llms/google-llms.md | 4 +- docs/modules/usage/llms/groq.md | 4 +- docs/modules/usage/llms/litellm-proxy.md | 2 +- docs/modules/usage/llms/llms.md | 2 +- docs/modules/usage/llms/openai-llms.md | 4 +- docs/modules/usage/llms/openrouter.md | 2 +- docs/sidebars.ts | 2 +- docs/src/pages/index.tsx | 11 ++ docs/static/img/settings-advanced.png | Bin 28576 -> 0 bytes docs/static/img/settings-screenshot.png | Bin 33797 -> 0 bytes 14 files changed, 101 insertions(+), 94 deletions(-) delete mode 100644 docs/static/img/settings-advanced.png delete mode 100644 docs/static/img/settings-screenshot.png diff --git a/docs/DOC_STYLE_GUIDE.md b/docs/DOC_STYLE_GUIDE.md index a55af799b112..93b916b0e85a 100644 --- a/docs/DOC_STYLE_GUIDE.md +++ b/docs/DOC_STYLE_GUIDE.md @@ -46,3 +46,11 @@ docker run -it \ -e THAT=that ... ``` + +### Referring to UI Elements + +When referencing UI elements, use ``. + +Example: +1. Toggle the `Advanced` option +2. Enter your model in the `Custom Model` textbox. diff --git a/docs/modules/usage/how-to/gui-mode.md b/docs/modules/usage/how-to/gui-mode.md index 483f8869e9eb..200e4ce3e0dc 100644 --- a/docs/modules/usage/how-to/gui-mode.md +++ b/docs/modules/usage/how-to/gui-mode.md @@ -1,9 +1,6 @@ # GUI Mode -## Introduction - -OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant. -This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI. +OpenHands provides a Graphical User Interface (GUI) mode for interacting with the AI assistant. ## Installation and Setup @@ -14,104 +11,95 @@ This mode offers an intuitive way to set up the environment, manage settings, an ### Initial Setup -1. Upon first launch, you'll see a settings modal. -2. Select an `LLM Provider` and `LLM Model` from the dropdown menus. +1. Upon first launch, you'll see a settings page. +2. Select an `LLM Provider` and `LLM Model` from the dropdown menus. If the required model does not exist in the list, + toggle `Advanced` options and enter it with the correct prefix in the `Custom Model` text box. 3. Enter the corresponding `API Key` for your chosen provider. -4. Click "Save" to apply the settings. +4. Click `Save Changes` to apply the settings. ### GitHub Token Setup OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways: -- **Locally (OSS)**: The user directly inputs their GitHub token. -- **Online (SaaS)**: The token is obtained through GitHub OAuth authentication. - -#### Setting Up a Local GitHub Token - -1. **Generate a Personal Access Token (PAT)**: - - Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic). - - Click "Generate new token (classic)". +- **Local Installation**: The user directly inputs their GitHub token. +
+ Setting Up a GitHub Token + 1. **Generate a Personal Access Token (PAT)**: + - On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic). + - Click `Generate new token (classic)`. - Required scopes: - `repo` (Full control of private repositories) - - `workflow` (Update GitHub Action workflows) - - `read:org` (Read organization data) + 2. **Enter Token in OpenHands**: + - Click the Settings button (gear icon). + - Navigate to the `GitHub Settings` section. + - Paste your token in the `GitHub Token` field. + - Click `Save Changes` to apply the changes. +
-2. **Enter Token in OpenHands**: - - Click the Settings button (gear icon) in the top right. - - Navigate to the "GitHub" section. - - Paste your token in the "GitHub Token" field. - - Click "Save" to apply the changes. +
+ Organizational Token Policies -#### Organizational Token Policies + If you're working with organizational repositories, additional setup may be required: -If you're working with organizational repositories, additional setup may be required: - -1. **Check Organization Requirements**: + 1. **Check Organization Requirements**: - Organization admins may enforce specific token policies. - Some organizations require tokens to be created with SSO enabled. - Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization). - -2. **Verify Organization Access**: + 2. **Verify Organization Access**: - Go to your token settings on GitHub. - - Look for the organization under "Organization access". - - If required, click "Enable SSO" next to your organization. + - Look for the organization under `Organization access`. + - If required, click `Enable SSO` next to your organization. - Complete the SSO authorization process. +
+ +
+ Troubleshooting + + Common issues and solutions: + + - **Token Not Recognized**: + - Ensure the token is properly saved in settings. + - Check that the token hasn't expired. + - Verify the token has the required scopes. + - Try regenerating the token. + + - **Organization Access Denied**: + - Check if SSO is required but not enabled. + - Verify organization membership. + - Contact organization admin if token policies are blocking access. -#### OAuth Authentication (Online Mode) + - **Verifying Token Works**: + - The app will show a green checkmark if the token is valid. + - Try accessing a repository to confirm permissions. + - Check the browser console for any error messages. +
-When using OpenHands in online mode, the GitHub OAuth flow: +- **OpenHands Cloud**: The token is obtained through GitHub OAuth authentication. -1. Requests the following permissions: +
+ OAuth Authentication + + When using OpenHands Cloud, the GitHub OAuth flow requests the following permissions: - Repository access (read/write) - Workflow management - Organization read access -2. Authentication steps: - - Click "Sign in with GitHub" when prompted. + To authenticate OpenHands: + - Click `Sign in with GitHub` when prompted. - Review the requested permissions. - Authorize OpenHands to access your GitHub account. - If using an organization, authorize organization access if prompted. - -#### Troubleshooting - -Common issues and solutions: - -- **Token Not Recognized**: - - Ensure the token is properly saved in settings. - - Check that the token hasn't expired. - - Verify the token has the required scopes. - - Try regenerating the token. - -- **Organization Access Denied**: - - Check if SSO is required but not enabled. - - Verify organization membership. - - Contact organization admin if token policies are blocking access. - -- **Verifying Token Works**: - - The app will show a green checkmark if the token is valid. - - Try accessing a repository to confirm permissions. - - Check the browser console for any error messages. - - Use the "Test Connection" button in settings if available. +
### Advanced Settings -1. Toggle `Advanced Options` to access additional settings. +1. Inside the Settings page, toggle `Advanced` options to access additional settings. 2. Use the `Custom Model` text box to manually enter a model if it's not in the list. 3. Specify a `Base URL` if required by your LLM provider. -### Main Interface - -The main interface consists of several key components: - -- **Chat Window**: The central area where you can view the conversation history with the AI assistant. -- **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI. -- **Send Button**: Click this to send your message to the AI. -- **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time. -- **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history. - ### Interacting with the AI -1. Type your question, request, or task description in the input box. +1. Type your prompt in the input box. 2. Click the send button or press Enter to submit your message. 3. The AI will process your input and provide a response in the chat window. 4. You can continue the conversation by asking follow-up questions or providing additional information. diff --git a/docs/modules/usage/installation.mdx b/docs/modules/usage/installation.mdx index 64a6014e7d8d..610be444fef4 100644 --- a/docs/modules/usage/installation.mdx +++ b/docs/modules/usage/installation.mdx @@ -12,7 +12,8 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
MacOS - ### Docker Desktop + + **Docker Desktop** 1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install). 2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled. @@ -25,7 +26,7 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to Tested with Ubuntu 22.04. ::: - ### Docker Desktop + **Docker Desktop** 1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/). @@ -33,12 +34,13 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
Windows - ### WSL + + **WSL** 1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install). 2. Run `wsl --version` in powershell and confirm `Default Version: 2`. - ### Docker Desktop + **Docker Desktop** 1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install). 2. Open Docker Desktop, go to `Settings` and confirm the following: @@ -78,24 +80,22 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod ## Setup -Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`. +Upon launching OpenHands, you'll see a Settings page. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`. These can be changed at any time by selecting the `Settings` button (gear icon) in the UI. -If the required `LLM Model` does not exist in the list, you can toggle `Advanced Options` and manually enter it with the correct prefix +If the required model does not exist in the list, you can toggle `Advanced` options and manually enter it with the correct prefix in the `Custom Model` text box. -The `Advanced Options` also allow you to specify a `Base URL` if required. +The `Advanced` options also allow you to specify a `Base URL` if required. -
- settings-modal - settings-modal -
+Now you're ready to [get started with OpenHands](./getting-started). ## Versions -The command above pulls the most recent stable release of OpenHands. You have other options as well: -- For a specific release, use `docker.all-hands.dev/all-hands-ai/openhands:$VERSION`, replacing $VERSION with the version number. -- We use semver, and release major, minor, and patch tags. So `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release. -- For the most up-to-date development version, you can use `docker.all-hands.dev/all-hands-ai/openhands:main`. This version is unstable and is recommended for testing or development purposes only. +The [docker command above](./installation#start-the-app) pulls the most recent stable release of OpenHands. You have other options as well: +- For a specific release, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with the version number. +We use SemVer so `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release. +- For the most up-to-date development version, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with `main`. +This version is unstable and is recommended for testing or development purposes only. You can choose the tag that best suits your needs based on stability requirements and desired features. diff --git a/docs/modules/usage/llms/azure-llms.md b/docs/modules/usage/llms/azure-llms.md index 7046fe7bf536..84f16627ab31 100644 --- a/docs/modules/usage/llms/azure-llms.md +++ b/docs/modules/usage/llms/azure-llms.md @@ -25,7 +25,7 @@ You will need your ChatGPT deployment name which can be found on the deployments <deployment-name> below. ::: -1. Enable `Advanced Options` +1. Enable `Advanced` options 2. Set the following: - `Custom Model` to azure/<deployment-name> - `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`) diff --git a/docs/modules/usage/llms/google-llms.md b/docs/modules/usage/llms/google-llms.md index d89ba389f057..74e9015ffb0a 100644 --- a/docs/modules/usage/llms/google-llms.md +++ b/docs/modules/usage/llms/google-llms.md @@ -10,7 +10,7 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings: - `LLM Provider` to `Gemini` - `LLM Model` to the model you will be using. -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`). - `API Key` to your Gemini API key ## VertexAI - Google Cloud Platform Configs @@ -27,4 +27,4 @@ VERTEXAI_LOCATION="" Then set the following in the OpenHands UI through the Settings: - `LLM Provider` to `VertexAI` - `LLM Model` to the model you will be using. -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>). diff --git a/docs/modules/usage/llms/groq.md b/docs/modules/usage/llms/groq.md index d484d5e3a4e1..0de104cf1400 100644 --- a/docs/modules/usage/llms/groq.md +++ b/docs/modules/usage/llms/groq.md @@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr - `LLM Provider` to `Groq` - `LLM Model` to the model you will be using. [Visit here to see the list of models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list, toggle -`Advanced Options`, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`). +`Advanced` options, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`). - `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys). @@ -17,7 +17,7 @@ models that Groq hosts](https://console.groq.com/docs/models). If the model is n The Groq endpoint for chat completion is [mostly OpenAI-compatible](https://console.groq.com/docs/openai). Therefore, you can access Groq models as you would access any OpenAI-compatible endpoint. In the OpenHands UI through the Settings: -1. Enable `Advanced Options` +1. Enable `Advanced` options 2. Set the following: - `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`) - `Base URL` to `https://api.groq.com/openai/v1` diff --git a/docs/modules/usage/llms/litellm-proxy.md b/docs/modules/usage/llms/litellm-proxy.md index 9178bc5c33ea..21413e0ef191 100644 --- a/docs/modules/usage/llms/litellm-proxy.md +++ b/docs/modules/usage/llms/litellm-proxy.md @@ -8,7 +8,7 @@ To use LiteLLM proxy with OpenHands, you need to: 1. Set up a LiteLLM proxy server (see [LiteLLM documentation](https://docs.litellm.ai/docs/proxy/quick_start)) 2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings: - * Enable `Advanced Options` + * Enable `Advanced` options * `Custom Model` to the prefix `litellm_proxy/` + the model you will be using (e.g. `litellm_proxy/anthropic.claude-3-5-sonnet-20241022-v2:0`) * `Base URL` to your LiteLLM proxy URL (e.g. `https://your-litellm-proxy.com`) * `API Key` to your LiteLLM proxy API key diff --git a/docs/modules/usage/llms/llms.md b/docs/modules/usage/llms/llms.md index f4fa118dd02e..c2b08d013491 100644 --- a/docs/modules/usage/llms/llms.md +++ b/docs/modules/usage/llms/llms.md @@ -38,7 +38,7 @@ The following can be set in the OpenHands UI through the Settings: - `LLM Provider` - `LLM Model` - `API Key` -- `Base URL` (through `Advanced Settings`) +- `Base URL` (through `Advanced` settings) There are some settings that may be necessary for some LLMs/providers that cannot be set through the UI. Instead, these can be set through environment variables passed to the [docker run command](/modules/usage/installation#start-the-app) diff --git a/docs/modules/usage/llms/openai-llms.md b/docs/modules/usage/llms/openai-llms.md index 9157c7cac8bb..d0358989691a 100644 --- a/docs/modules/usage/llms/openai-llms.md +++ b/docs/modules/usage/llms/openai-llms.md @@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr * `LLM Provider` to `OpenAI` * `LLM Model` to the model you will be using. [Visit here to see a full list of OpenAI models that LiteLLM supports.](https://docs.litellm.ai/docs/providers/openai#openai-chat-completion-models) -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`). * `API Key` to your OpenAI API key. To find or create your OpenAI Project API Key, [see here](https://platform.openai.com/api-keys). ## Using OpenAI-Compatible Endpoints @@ -18,7 +18,7 @@ Just as for OpenAI Chat completions, we use LiteLLM for OpenAI-compatible endpoi ## Using an OpenAI Proxy If you're using an OpenAI proxy, in the OpenHands UI through the Settings: -1. Enable `Advanced Options` +1. Enable `Advanced` options 2. Set the following: - `Custom Model` to openai/<model-name> (e.g. `openai/gpt-4o` or openai/<proxy-prefix>/<model-name>) - `Base URL` to the URL of your OpenAI proxy diff --git a/docs/modules/usage/llms/openrouter.md b/docs/modules/usage/llms/openrouter.md index 247d0a0558f1..2b5204d26c82 100644 --- a/docs/modules/usage/llms/openrouter.md +++ b/docs/modules/usage/llms/openrouter.md @@ -8,5 +8,5 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr * `LLM Provider` to `OpenRouter` * `LLM Model` to the model you will be using. [Visit here to see a full list of OpenRouter models](https://openrouter.ai/models). -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`). * `API Key` to your OpenRouter API key. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index da416ac30b91..f71e36a0b571 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -66,7 +66,7 @@ const sidebars: SidebarsConfig = { }, { type: 'doc', - label: 'Github Actions', + label: 'Github Action', id: 'usage/how-to/github-action', }, { diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index a2df79a259a5..6f20f1eb776d 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -23,6 +23,17 @@ export default function Home(): JSX.Element { })} > + ); } diff --git a/docs/static/img/settings-advanced.png b/docs/static/img/settings-advanced.png deleted file mode 100644 index 43a9cf05ab83383c0dfafeba871bd1cc89cbf7ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28576 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 diff --git a/docs/static/img/settings-screenshot.png b/docs/static/img/settings-screenshot.png deleted file mode 100644 index 987dd8c255704a3e1ecb803658a0533f1f75627b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33797 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 z zTs*JJhPWt%*X47kTubK~nSi(?rg%4%wpt&V@DJQ-hkrHEMGPIyw<4DHO8yNnnr*sEiDbQgljZU1qXu`Ehi^7 z3JS3Sd#~LcsSB{_9#H3vU;;ClBs6qSya11$0w7S|)~11H_a0Up5-mr8(8pFu7dVZv-$*YaLNAogTOtF^U9bCM`*+rK zof5DuB5h0dW9k9Lj5nO>AMuoHVE}GoIz1`G+>nJ`rdT=pWy;tg`GRm$FZUv-`HgbT zJ_(E2{*g?TA$0V57G{}+>Se4_Cseee#6y>p7V$rS585H1d0wKG2 z#3VuAo-M2?A_*jZ4GB$>^j0yd$8anNOo5gfxNwV@2Gpk~{Sz8zlBh@giNRmDJ_{sp zb(s4cDG>$Le_IxmS}MQ&z@ZQo(YBh(*W|HE@kM*#ZgVt_f}(-$HGF+@mATO0LC zEuyw=No*sWB?EKt%eaKHND@C>+Nhqb2#7{pD%_xH4dRw}1b*mscbu2&nZ{5=SWQuz zX2ze8jQSP*2Rjcwbt*^P1#AfEj@NH*`@I`Qcl++#$IF1REgoS7rS2HA;ujgg9HbP~ z@h3w&Y8JTBPq(&aA&%&7mlYqg*ii{-qdTh%Y`+g%iW{EhUC2ad_f85)_V1hVM$Nk_ z4E(|NmXx27y;teBuN`mgi=MJrop@t*+bcz-uV)ebJI3dk;S7Fpnd5B=oV30T#*S{O z#a%M>r&QHH`m$y^mv1RJSzjftWXIh~W5BqPnbr5JVl+=K`te6^=^x&H^5ExfuN_WR zHF-BT??;*`6wEC*(C~9ZOtpHh{Pqu@EkUsPe#A=Q=;M(Xy?T0|_|FIF9VctE-+e~a z?%k&ov&tSAe-1FGM_rEBvTbj?mNf+~jbWbj?$~tiZVS#B9@H(4i;7OxvF%5X zHQT*ES^P!upb-TcrElc1m~y`4yFju69OtGV`5B8&5l)M)b!z7pm#w^P+zgH9R{6mW ztx&Bv>0dWe=wI5=XW|h=)KsuN6eA|kT%!LiuObNEu+yD5maJ$IW`9v0(kB_RU6=`c z$gbYcq$VZWzAwlwMahvdjd#W=D3hskXg!*?OP$Pp`pDZdKXXys@yTK^9qy8ac)xi2 zmKfvsEb&$ROif6eI>lPmOcNbTPt7mo0KJK=aN}fJ&j~gOdu@KL&2Q7M|Ijy|t)EC* z3Jz=pORe`zqs6^mS&N?k!p>EG(TUH9`#D(>mWmA#v7Wi(twaQaL{lPacrOhG9S*I6 zrZ~m#&6}oK)9TXfXUzvQdCSY!5>4F35xu7k@N+EPqJ`TQ237_1no zD2@K61TiYXjX|1Ed_>~I^tB2__7q|u>67AL*6!4=L~o_J;bqKIS@GE zQyw#1$k@F>-&r~2o331N>KP~_IhXN)gXe;&le}T2klv%t#9F$5V zu_(OLfnuuoZ}j9FvVthwP0p2z0mLPI!^_@03@--O5ir@`m0!INMbegG))p3P7Hbk5>Zf$ zcWjT7cz)K38rBgk{QMfkQKLlAGx}gni~4IJxkD64{i?s#+4{=*mtM08u4B#GtIB)-cl~&bVkMQE zWreZ=^TLVm369TqAI^D{Rl}304l1oWOsg5hJ5hxDS$DMZ&#M)49(CqDgG;8B`AKEu zHD2h(^|)TTSj+85#J#-L&Tn2Ad)hZz{BkSGLlNhHtJ^`+5TOuVDM53qIQV4fc-fP= zo;4zQ;kc&qA&O~aIFLg<>ardLb+&rMi+zqQhuy?70vq{;R`o0Atk3&&nU+*MWQl9VWZ=dE5Z`KY^4oJD-U8kS(FYXuTE zvIp!VL&tMc1o!{a(T60T-q)+e3j!pO(1Zfd4HI^ Date: Wed, 19 Feb 2025 20:40:40 +0100 Subject: [PATCH 05/41] Clean up NullObservations from the stream (#6260) --- openhands/agenthub/dummy_agent/agent.py | 3 +-- openhands/core/cli.py | 26 ++++++++++++------------- openhands/events/utils.py | 4 +++- openhands/runtime/base.py | 3 +++ 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/openhands/agenthub/dummy_agent/agent.py b/openhands/agenthub/dummy_agent/agent.py index f7a654bf75b4..b420a3d5d8ae 100644 --- a/openhands/agenthub/dummy_agent/agent.py +++ b/openhands/agenthub/dummy_agent/agent.py @@ -22,7 +22,6 @@ CmdOutputObservation, FileReadObservation, FileWriteObservation, - NullObservation, Observation, ) from openhands.events.serialization.event import event_to_dict @@ -109,7 +108,7 @@ def __init__(self, llm: LLM, config: AgentConfig): }, { 'action': AgentRejectAction(), - 'observations': [NullObservation('')], + 'observations': [AgentStateChangedObservation('', AgentState.REJECTED)], }, { 'action': AgentFinishAction( diff --git a/openhands/core/cli.py b/openhands/core/cli.py index 1e31537155ac..34186cd5fc8a 100644 --- a/openhands/core/cli.py +++ b/openhands/core/cli.py @@ -29,7 +29,6 @@ AgentStateChangedObservation, CmdOutputObservation, FileEditObservation, - NullObservation, ) @@ -143,19 +142,18 @@ async def on_event_async(event: Event): AgentState.FINISHED, ]: await prompt_for_next_task() - if ( - isinstance(event, NullObservation) - and controller.state.agent_state == AgentState.AWAITING_USER_CONFIRMATION - ): - user_confirmed = await prompt_for_user_confirmation() - if user_confirmed: - event_stream.add_event( - ChangeAgentStateAction(AgentState.USER_CONFIRMED), EventSource.USER - ) - else: - event_stream.add_event( - ChangeAgentStateAction(AgentState.USER_REJECTED), EventSource.USER - ) + if event.agent_state == AgentState.AWAITING_USER_CONFIRMATION: + user_confirmed = await prompt_for_user_confirmation() + if user_confirmed: + event_stream.add_event( + ChangeAgentStateAction(AgentState.USER_CONFIRMED), + EventSource.USER, + ) + else: + event_stream.add_event( + ChangeAgentStateAction(AgentState.USER_REJECTED), + EventSource.USER, + ) def on_event(event: Event) -> None: loop.create_task(on_event_async(event)) diff --git a/openhands/events/utils.py b/openhands/events/utils.py index bf710edcd7bd..cfc2dd804c3a 100644 --- a/openhands/events/utils.py +++ b/openhands/events/utils.py @@ -10,7 +10,9 @@ def get_pairs_from_events(events: list[Event]) -> list[tuple[Action, Observation]]: - """Return the history as a list of tuples (action, observation).""" + """Return the history as a list of tuples (action, observation). + + This function is a compatibility function for evals reading and visualization working with old histories.""" tuples: list[tuple[Action, Observation]] = [] action_map: dict[int, Action] = {} observation_map: dict[int, Observation] = {} diff --git a/openhands/runtime/base.py b/openhands/runtime/base.py index 983fc67fa898..8cdd17e18ead 100644 --- a/openhands/runtime/base.py +++ b/openhands/runtime/base.py @@ -254,6 +254,9 @@ async def _handle_action(self, event: Action) -> None: # this might be unnecessary, since source should be set by the event stream when we're here source = event.source if event.source else EventSource.AGENT + if isinstance(observation, NullObservation): + # don't add null observations to the event stream + return self.event_stream.add_event(observation, source) # type: ignore[arg-type] def clone_repo( From eed7e2dd6e4b94a3e69e3f287068090978198ac4 Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Wed, 19 Feb 2025 22:10:14 +0100 Subject: [PATCH 06/41] Refactor I/O utils; allow 'task' command line parameter in cli.py (#6187) Co-authored-by: OpenHands Bot --- openhands/agenthub/micro/agent.py | 2 +- openhands/core/cli.py | 36 +++++++++---------- openhands/core/main.py | 52 ++++++---------------------- openhands/core/utils/__init__.py | 0 openhands/events/stream.py | 2 +- openhands/io/__init__.py | 10 ++++++ openhands/io/io.py | 40 +++++++++++++++++++++ openhands/{core/utils => io}/json.py | 0 openhands/llm/llm.py | 4 +-- tests/unit/test_cli.py | 6 ++-- tests/unit/test_json.py | 2 +- tests/unit/test_json_encoder.py | 2 +- tests/unit/test_response_parsing.py | 2 +- 13 files changed, 88 insertions(+), 70 deletions(-) delete mode 100644 openhands/core/utils/__init__.py create mode 100644 openhands/io/__init__.py create mode 100644 openhands/io/io.py rename openhands/{core/utils => io}/json.py (100%) diff --git a/openhands/agenthub/micro/agent.py b/openhands/agenthub/micro/agent.py index 2c22e3840a51..37de035c461d 100644 --- a/openhands/agenthub/micro/agent.py +++ b/openhands/agenthub/micro/agent.py @@ -6,11 +6,11 @@ from openhands.controller.state.state import State from openhands.core.config import AgentConfig from openhands.core.message import ImageContent, Message, TextContent -from openhands.core.utils import json from openhands.events.action import Action from openhands.events.event import Event from openhands.events.serialization.action import action_from_dict from openhands.events.serialization.event import event_to_memory +from openhands.io import json from openhands.llm.llm import LLM diff --git a/openhands/core/cli.py b/openhands/core/cli.py index 34186cd5fc8a..05a390d8b815 100644 --- a/openhands/core/cli.py +++ b/openhands/core/cli.py @@ -30,6 +30,7 @@ CmdOutputObservation, FileEditObservation, ) +from openhands.io import read_input, read_task def display_message(message: str): @@ -82,21 +83,6 @@ def display_event(event: Event, config: AppConfig): display_confirmation(event.confirmation_state) -def read_input(config: AppConfig) -> str: - """Read input from user based on config settings.""" - if config.cli_multiline_input: - print('Enter your message (enter "/exit" on a new line to finish):') - lines = [] - while True: - line = input('>> ').rstrip() - if line == '/exit': # finish input - break - lines.append(line) - return '\n'.join(lines) - else: - return input('>> ').rstrip() - - async def main(loop: asyncio.AbstractEventLoop): """Runs the agent in CLI mode.""" @@ -104,7 +90,14 @@ async def main(loop: asyncio.AbstractEventLoop): logger.setLevel(logging.WARNING) - config = setup_config_from_args(args) + # Load config from toml and override with command line arguments + config: AppConfig = setup_config_from_args(args) + + # Read task from file, CLI args, or stdin + task_str = read_task(args, config.cli_multiline_input) + + # If we have a task, create initial user action + initial_user_action = MessageAction(content=task_str) if task_str else None sid = str(uuid4()) @@ -117,7 +110,9 @@ async def main(loop: asyncio.AbstractEventLoop): async def prompt_for_next_task(): # Run input() in a thread pool to avoid blocking the event loop - next_message = await loop.run_in_executor(None, read_input, config) + next_message = await loop.run_in_executor( + None, read_input, config.cli_multiline_input + ) if not next_message.strip(): await prompt_for_next_task() if next_message == 'exit': @@ -162,7 +157,12 @@ def on_event(event: Event) -> None: await runtime.connect() - asyncio.create_task(prompt_for_next_task()) + if initial_user_action: + # If there's an initial user action, enqueue it and do not prompt again + event_stream.add_event(initial_user_action, EventSource.USER) + else: + # Otherwise prompt for the user's first message right away + asyncio.create_task(prompt_for_next_task()) await run_agent_until_done( controller, runtime, [AgentState.STOPPED, AgentState.ERROR] diff --git a/openhands/core/main.py b/openhands/core/main.py index 2652931cce7a..12e0c4e7876c 100644 --- a/openhands/core/main.py +++ b/openhands/core/main.py @@ -1,7 +1,6 @@ import asyncio import json import os -import sys from pathlib import Path from typing import Callable, Protocol @@ -29,6 +28,7 @@ from openhands.events.observation import AgentStateChangedObservation from openhands.events.serialization import event_from_dict from openhands.events.serialization.event import event_to_trajectory +from openhands.io import read_input, read_task from openhands.runtime.base import Runtime @@ -41,32 +41,6 @@ def __call__( ) -> str: ... -def read_task_from_file(file_path: str) -> str: - """Read task from the specified file.""" - with open(file_path, 'r', encoding='utf-8') as file: - return file.read() - - -def read_task_from_stdin() -> str: - """Read task from stdin.""" - return sys.stdin.read() - - -def read_input(config: AppConfig) -> str: - """Read input from user based on config settings.""" - if config.cli_multiline_input: - print('Enter your message (enter "/exit" on a new line to finish):') - lines = [] - while True: - line = input('>> ').rstrip() - if line == '/exit': # finish input - break - lines.append(line) - return '\n'.join(lines) - else: - return input('>> ').rstrip() - - async def run_controller( config: AppConfig, initial_user_action: Action, @@ -139,7 +113,6 @@ async def run_controller( assert isinstance( initial_user_action, Action ), f'initial user actions must be an Action, got {type(initial_user_action)}' - # Logging logger.debug( f'Agent Controller Initialized: Running agent {agent.name}, model ' f'{agent.llm.config.model}, with actions: {initial_user_action}' @@ -167,7 +140,7 @@ def on_event(event: Event): if exit_on_message: message = '/exit' elif fake_user_response_fn is None: - message = read_input(config) + message = read_input(config.cli_multiline_input) else: message = fake_user_response_fn(controller.get_state()) action = MessageAction(content=message) @@ -268,28 +241,23 @@ def load_replay_log(trajectory_path: str) -> tuple[list[Event] | None, Action]: if __name__ == '__main__': args = parse_arguments() - config = setup_config_from_args(args) + config: AppConfig = setup_config_from_args(args) - # Determine the task - task_str = '' - if args.file: - task_str = read_task_from_file(args.file) - elif args.task: - task_str = args.task - elif not sys.stdin.isatty(): - task_str = read_task_from_stdin() + # Read task from file, CLI args, or stdin + task_str = read_task(args, config.cli_multiline_input) - initial_user_action: Action = NullAction() if config.replay_trajectory_path: if task_str: raise ValueError( 'User-specified task is not supported under trajectory replay mode' ) - elif task_str: - initial_user_action = MessageAction(content=task_str) - else: + + if not task_str: raise ValueError('No task provided. Please specify a task through -t, -f.') + # Create initial user action + initial_user_action: MessageAction = MessageAction(content=task_str) + # Set session name session_name = args.name sid = generate_sid(config, session_name) diff --git a/openhands/core/utils/__init__.py b/openhands/core/utils/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/openhands/events/stream.py b/openhands/events/stream.py index 0fc547803f6d..938269822a7a 100644 --- a/openhands/events/stream.py +++ b/openhands/events/stream.py @@ -8,9 +8,9 @@ from typing import Callable, Iterable from openhands.core.logger import openhands_logger as logger -from openhands.core.utils import json from openhands.events.event import Event, EventSource from openhands.events.serialization.event import event_from_dict, event_to_dict +from openhands.io import json from openhands.storage import FileStore from openhands.storage.locations import ( get_conversation_dir, diff --git a/openhands/io/__init__.py b/openhands/io/__init__.py new file mode 100644 index 000000000000..bf1a054356c1 --- /dev/null +++ b/openhands/io/__init__.py @@ -0,0 +1,10 @@ +from openhands.io.io import read_input, read_task, read_task_from_file +from openhands.io.json import dumps, loads + +__all__ = [ + 'read_input', + 'read_task_from_file', + 'read_task', + 'dumps', + 'loads', +] diff --git a/openhands/io/io.py b/openhands/io/io.py new file mode 100644 index 000000000000..2e42df912b77 --- /dev/null +++ b/openhands/io/io.py @@ -0,0 +1,40 @@ +import argparse +import sys + + +def read_input(cli_multiline_input: bool = False) -> str: + """Read input from user based on config settings.""" + if cli_multiline_input: + print('Enter your message (enter "/exit" on a new line to finish):') + lines = [] + while True: + line = input('>> ').rstrip() + if line == '/exit': # finish input + break + lines.append(line) + return '\n'.join(lines) + else: + return input('>> ').rstrip() + + +def read_task_from_file(file_path: str) -> str: + """Read task from the specified file.""" + with open(file_path, 'r', encoding='utf-8') as file: + return file.read() + + +def read_task(args: argparse.Namespace, cli_multiline_input: bool) -> str: + """ + Read the task from the CLI args, file, or stdin. + """ + + # Determine the task + task_str = '' + if args.file: + task_str = read_task_from_file(args.file) + elif args.task: + task_str = args.task + elif not sys.stdin.isatty(): + task_str = read_input(cli_multiline_input) + + return task_str diff --git a/openhands/core/utils/json.py b/openhands/io/json.py similarity index 100% rename from openhands/core/utils/json.py rename to openhands/io/json.py diff --git a/openhands/llm/llm.py b/openhands/llm/llm.py index a9071b43bed3..b40f11ca8396 100644 --- a/openhands/llm/llm.py +++ b/openhands/llm/llm.py @@ -172,7 +172,7 @@ def __init__( ) def wrapper(*args, **kwargs): """Wrapper for the litellm completion function. Logs the input and output of the completion function.""" - from openhands.core.utils import json + from openhands.io import json messages: list[dict[str, Any]] | dict[str, Any] = [] mock_function_calling = not self.is_function_calling_active() @@ -369,7 +369,7 @@ def init_model_info(self): # noinspection PyBroadException except Exception: pass - from openhands.core.utils import json + from openhands.io import json logger.debug(f'Model info: {json.dumps(self.model_info, indent=2)}') diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 520d85d2aa7d..3931f2fdd713 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -1,7 +1,7 @@ from unittest.mock import patch -from openhands.core.cli import read_input from openhands.core.config import AppConfig +from openhands.io import read_input def test_single_line_input(): @@ -10,7 +10,7 @@ def test_single_line_input(): config.cli_multiline_input = False with patch('builtins.input', return_value='hello world'): - result = read_input(config) + result = read_input(config.cli_multiline_input) assert result == 'hello world' @@ -23,5 +23,5 @@ def test_multiline_input(): mock_inputs = ['line 1', 'line 2', 'line 3', '/exit'] with patch('builtins.input', side_effect=mock_inputs): - result = read_input(config) + result = read_input(config.cli_multiline_input) assert result == 'line 1\nline 2\nline 3' diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py index 883efdfe4cfb..85ab265a536d 100644 --- a/tests/unit/test_json.py +++ b/tests/unit/test_json.py @@ -1,7 +1,7 @@ from datetime import datetime -from openhands.core.utils import json from openhands.events.action import MessageAction +from openhands.io import json def test_event_serialization_deserialization(): diff --git a/tests/unit/test_json_encoder.py b/tests/unit/test_json_encoder.py index daa2708a6256..10058c8c2ba3 100644 --- a/tests/unit/test_json_encoder.py +++ b/tests/unit/test_json_encoder.py @@ -3,7 +3,7 @@ import psutil -from openhands.core.utils.json import dumps +from openhands.io.json import dumps def get_memory_usage(): diff --git a/tests/unit/test_response_parsing.py b/tests/unit/test_response_parsing.py index fd588d4c6edf..dc51dee3abe4 100644 --- a/tests/unit/test_response_parsing.py +++ b/tests/unit/test_response_parsing.py @@ -2,11 +2,11 @@ from openhands.agenthub.micro.agent import parse_response as parse_response_micro from openhands.core.exceptions import LLMResponseError -from openhands.core.utils.json import loads as custom_loads from openhands.events.action import ( FileWriteAction, MessageAction, ) +from openhands.io import loads as custom_loads @pytest.mark.parametrize( From 74c942c91110096d2a121daae904a4ed6a2a8e42 Mon Sep 17 00:00:00 2001 From: Calvin Smith Date: Wed, 19 Feb 2025 14:17:48 -0700 Subject: [PATCH 07/41] fix: LLM summarization prompt handles user messages (#6837) Co-authored-by: Calvin Smith --- .../condenser/impl/llm_summarizing_condenser.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openhands/memory/condenser/impl/llm_summarizing_condenser.py b/openhands/memory/condenser/impl/llm_summarizing_condenser.py index 276d1c1e8748..54068f04547c 100644 --- a/openhands/memory/condenser/impl/llm_summarizing_condenser.py +++ b/openhands/memory/condenser/impl/llm_summarizing_condenser.py @@ -57,22 +57,29 @@ def condense(self, events: list[Event]) -> list[Event]: # Construct prompt for summarization prompt = """You are maintaining state history for an LLM-based code agent. Track: +USER_CONTEXT: (Preserve essential user requirements, problem descriptions, and clarifications in concise form) + STATE: {File paths, function signatures, data structures} TESTS: {Failing cases, error messages, outputs} CHANGES: {Code edits, variable updates} DEPS: {Dependencies, imports, external calls} INTENT: {Why changes were made, acceptance criteria} -SKIP: {Git clones, build logs} -SUMMARIZE: {File listings} -MAX_LENGTH: Keep summaries under 1000 words +PRIORITIZE: +1. Capture key user requirements and constraints +2. Maintain critical problem context +3. Keep all sections concise + +SKIP: {Git clones, build logs, file listings} Example history format: +USER_CONTEXT: Fix FITS card float representation - "0.009125" becomes "0.009124999999999999" causing comment truncation. Use Python's str() when possible while maintaining FITS compliance. + STATE: mod_float() in card.py updated TESTS: test_format() passed CHANGES: str(val) replaces f"{val:.16G}" DEPS: None modified -INTENT: Fix float precision overflow""" +INTENT: Fix precision while maintaining FITS compliance""" prompt + '\n\n' From f869ad995cc9e871ea59d99844c231e7bd035db4 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:58:09 +0400 Subject: [PATCH 08/41] hotfix: Remove external link in billing settings UI (#6841) --- .../components/features/payment/payment-form.test.tsx | 6 ------ frontend/src/components/features/payment/payment-form.tsx | 8 -------- 2 files changed, 14 deletions(-) diff --git a/frontend/__tests__/components/features/payment/payment-form.test.tsx b/frontend/__tests__/components/features/payment/payment-form.test.tsx index 815d0530d103..323597693901 100644 --- a/frontend/__tests__/components/features/payment/payment-form.test.tsx +++ b/frontend/__tests__/components/features/payment/payment-form.test.tsx @@ -78,12 +78,6 @@ describe("PaymentForm", () => { expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13); }); - it("should render the payment method link", async () => { - renderPaymentForm(); - - screen.getByTestId("payment-methods-link"); - }); - it("should disable the top-up button if the user enters an invalid amount", async () => { const user = userEvent.setup(); renderPaymentForm(); diff --git a/frontend/src/components/features/payment/payment-form.tsx b/frontend/src/components/features/payment/payment-form.tsx index e42a9e7894d0..db57a3667286 100644 --- a/frontend/src/components/features/payment/payment-form.tsx +++ b/frontend/src/components/features/payment/payment-form.tsx @@ -5,7 +5,6 @@ import { cn } from "#/utils/utils"; import MoneyIcon from "#/icons/money.svg?react"; import { SettingsInput } from "../settings/settings-input"; import { BrandButton } from "../settings/brand-button"; -import { HelpLink } from "../settings/help-link"; import { LoadingSpinner } from "#/components/shared/loading-spinner"; import { amountIsValid } from "#/utils/amount-is-valid"; @@ -80,13 +79,6 @@ export function PaymentForm() { {isPending && } - - ); } From 3f8bc8a7ea2fc634c2cc98c1349fef85862eabb8 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:58:23 +0400 Subject: [PATCH 09/41] hotfix: Set proper minimum and maximum defaults that can be entered in billing input (#6842) --- .../features/payment/payment-form.test.tsx | 2 +- frontend/__tests__/utils/amount-is-valid.test.ts | 12 +++++++++--- frontend/src/utils/amount-is-valid.ts | 4 +++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/__tests__/components/features/payment/payment-form.test.tsx b/frontend/__tests__/components/features/payment/payment-form.test.tsx index 323597693901..2d30f45fad5a 100644 --- a/frontend/__tests__/components/features/payment/payment-form.test.tsx +++ b/frontend/__tests__/components/features/payment/payment-form.test.tsx @@ -149,7 +149,7 @@ describe("PaymentForm", () => { renderPaymentForm(); const topUpInput = await screen.findByTestId("top-up-input"); - await user.type(topUpInput, "20"); // test assumes the minimum is 25 + await user.type(topUpInput, "9"); // test assumes the minimum is 10 const topUpButton = screen.getByText("Add credit"); await user.click(topUpButton); diff --git a/frontend/__tests__/utils/amount-is-valid.test.ts b/frontend/__tests__/utils/amount-is-valid.test.ts index 30181e1b48c9..3a940f77e0ad 100644 --- a/frontend/__tests__/utils/amount-is-valid.test.ts +++ b/frontend/__tests__/utils/amount-is-valid.test.ts @@ -24,9 +24,15 @@ describe("amountIsValid", () => { }); test("when an amount less than the minimum is passed", () => { - // test assumes the minimum is 25 - expect(amountIsValid("24")).toBe(false); - expect(amountIsValid("24.99")).toBe(false); + // test assumes the minimum is 10 + expect(amountIsValid("9")).toBe(false); + expect(amountIsValid("9.99")).toBe(false); + }); + + test("when an amount more than the maximum is passed", () => { + // test assumes the minimum is 25000 + expect(amountIsValid("25001")).toBe(false); + expect(amountIsValid("25000.01")).toBe(false); }); }); }); diff --git a/frontend/src/utils/amount-is-valid.ts b/frontend/src/utils/amount-is-valid.ts index c987fcecff17..0912221f9352 100644 --- a/frontend/src/utils/amount-is-valid.ts +++ b/frontend/src/utils/amount-is-valid.ts @@ -1,10 +1,12 @@ -const MINIMUM_AMOUNT = 25; +const MINIMUM_AMOUNT = 10; +const MAXIMUM_AMOUNT = 25_000; export const amountIsValid = (amount: string) => { const float = parseFloat(amount); if (Number.isNaN(float)) return false; if (float < 0) return false; if (float < MINIMUM_AMOUNT) return false; + if (float > MAXIMUM_AMOUNT) return false; return true; }; From 42f1fc92fa4330a9af38ac700aeb762a9a8cc1ad Mon Sep 17 00:00:00 2001 From: tofarr Date: Thu, 20 Feb 2025 14:56:20 +0000 Subject: [PATCH 10/41] Fix: Less squashed logo (#6853) --- .../src/components/shared/buttons/all-hands-logo-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/shared/buttons/all-hands-logo-button.tsx b/frontend/src/components/shared/buttons/all-hands-logo-button.tsx index e068f152af13..dfa015839f0d 100644 --- a/frontend/src/components/shared/buttons/all-hands-logo-button.tsx +++ b/frontend/src/components/shared/buttons/all-hands-logo-button.tsx @@ -12,7 +12,7 @@ export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) { ariaLabel="All Hands Logo" onClick={onClick} > - + ); } From 52723061b1323e20751afe32adfa4b759e7335a9 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 20 Feb 2025 10:50:17 -0500 Subject: [PATCH 11/41] Add conversation age limit configuration (#6763) Co-authored-by: openhands --- openhands/core/config/app_config.py | 1 + .../server/routes/manage_conversations.py | 18 +++- tests/unit/test_conversation.py | 88 ++++++++++++------- 3 files changed, 72 insertions(+), 35 deletions(-) diff --git a/openhands/core/config/app_config.py b/openhands/core/config/app_config.py index 8c995d1ee3db..5965f06480c8 100644 --- a/openhands/core/config/app_config.py +++ b/openhands/core/config/app_config.py @@ -76,6 +76,7 @@ class AppConfig(BaseModel): file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*']) runloop_api_key: SecretStr | None = Field(default=None) cli_multiline_input: bool = Field(default=False) + conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds defaults_dict: ClassVar[dict] = {} diff --git a/openhands/server/routes/manage_conversations.py b/openhands/server/routes/manage_conversations.py index 0209fcbae4fe..7711cbb9e824 100644 --- a/openhands/server/routes/manage_conversations.py +++ b/openhands/server/routes/manage_conversations.py @@ -130,8 +130,9 @@ async def _create_new_conversation( @app.post('/conversations') async def new_conversation(request: Request, data: InitSessionRequest): """Initialize a new session or join an existing one. + After successful initialization, the client should connect to the WebSocket - using the returned conversation ID + using the returned conversation ID. """ logger.info('Initializing new conversation') user_id = get_user_id(request) @@ -188,10 +189,19 @@ async def search_conversations( config, get_user_id(request) ) conversation_metadata_result_set = await conversation_store.search(page_id, limit) + + # Filter out conversations older than max_age + now = datetime.now(timezone.utc) + max_age = config.conversation_max_age_seconds + filtered_results = [ + conversation for conversation in conversation_metadata_result_set.results + if hasattr(conversation, 'created_at') and + (now - conversation.created_at.replace(tzinfo=timezone.utc)).total_seconds() <= max_age + ] + conversation_ids = set( conversation.conversation_id - for conversation in conversation_metadata_result_set.results - if hasattr(conversation, 'created_at') + for conversation in filtered_results ) running_conversations = await conversation_manager.get_running_agent_loops( get_user_id(request), set(conversation_ids) @@ -202,7 +212,7 @@ async def search_conversations( conversation=conversation, is_running=conversation.conversation_id in running_conversations, ) - for conversation in conversation_metadata_result_set.results + for conversation in filtered_results ), next_page_id=conversation_metadata_result_set.next_page_id, ) diff --git a/tests/unit/test_conversation.py b/tests/unit/test_conversation.py index 5efb44294a53..e9e61aa88ca0 100644 --- a/tests/unit/test_conversation.py +++ b/tests/unit/test_conversation.py @@ -1,10 +1,11 @@ import json from contextlib import contextmanager -from datetime import datetime +from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest +from openhands.runtime.impl.docker.docker_runtime import DockerRuntime from openhands.server.routes.manage_conversations import ( delete_conversation, get_conversation, @@ -30,8 +31,8 @@ def _patch_store(): 'selected_repository': 'foobar', 'conversation_id': 'some_conversation_id', 'github_user_id': '12345', - 'created_at': '2025-01-01T00:00:00', - 'last_updated_at': '2025-01-01T00:01:00', + 'created_at': '2025-01-01T00:00:00+00:00', + 'last_updated_at': '2025-01-01T00:01:00+00:00', } ), ) @@ -49,22 +50,46 @@ def _patch_store(): @pytest.mark.asyncio async def test_search_conversations(): with _patch_store(): - result_set = await search_conversations( - MagicMock(state=MagicMock(github_token='')) - ) - expected = ConversationInfoResultSet( - results=[ - ConversationInfo( - conversation_id='some_conversation_id', - title='Some Conversation', - created_at=datetime.fromisoformat('2025-01-01T00:00:00'), - last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'), - status=ConversationStatus.STOPPED, - selected_repository='foobar', - ) - ] - ) - assert result_set == expected + with patch( + 'openhands.server.routes.manage_conversations.config' + ) as mock_config: + mock_config.conversation_max_age_seconds = 864000 # 10 days + with patch( + 'openhands.server.routes.manage_conversations.conversation_manager' + ) as mock_manager: + + async def mock_get_running_agent_loops(*args, **kwargs): + return set() + + mock_manager.get_running_agent_loops = mock_get_running_agent_loops + with patch( + 'openhands.server.routes.manage_conversations.datetime' + ) as mock_datetime: + mock_datetime.now.return_value = datetime.fromisoformat( + '2025-01-01T00:00:00+00:00' + ) + mock_datetime.fromisoformat = datetime.fromisoformat + mock_datetime.timezone = timezone + result_set = await search_conversations( + MagicMock(state=MagicMock(github_token='')) + ) + expected = ConversationInfoResultSet( + results=[ + ConversationInfo( + conversation_id='some_conversation_id', + title='Some Conversation', + created_at=datetime.fromisoformat( + '2025-01-01T00:00:00+00:00' + ), + last_updated_at=datetime.fromisoformat( + '2025-01-01T00:01:00+00:00' + ), + status=ConversationStatus.STOPPED, + selected_repository='foobar', + ) + ] + ) + assert result_set == expected @pytest.mark.asyncio @@ -76,8 +101,8 @@ async def test_get_conversation(): expected = ConversationInfo( conversation_id='some_conversation_id', title='Some Conversation', - created_at=datetime.fromisoformat('2025-01-01T00:00:00'), - last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'), + created_at=datetime.fromisoformat('2025-01-01T00:00:00+00:00'), + last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00+00:00'), status=ConversationStatus.STOPPED, selected_repository='foobar', ) @@ -109,8 +134,8 @@ async def test_update_conversation(): expected = ConversationInfo( conversation_id='some_conversation_id', title='New Title', - created_at=datetime.fromisoformat('2025-01-01T00:00:00'), - last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'), + created_at=datetime.fromisoformat('2025-01-01T00:00:00+00:00'), + last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00+00:00'), status=ConversationStatus.STOPPED, selected_repository='foobar', ) @@ -120,11 +145,12 @@ async def test_update_conversation(): @pytest.mark.asyncio async def test_delete_conversation(): with _patch_store(): - await delete_conversation( - 'some_conversation_id', - MagicMock(state=MagicMock(github_token='')), - ) - conversation = await get_conversation( - 'some_conversation_id', MagicMock(state=MagicMock(github_token='')) - ) - assert conversation is None + with patch.object(DockerRuntime, 'delete', return_value=None): + await delete_conversation( + 'some_conversation_id', + MagicMock(state=MagicMock(github_token='')), + ) + conversation = await get_conversation( + 'some_conversation_id', MagicMock(state=MagicMock(github_token='')) + ) + assert conversation is None From 2f14e537467a34720bfd3bf39a3359c05bcba2bf Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:13:50 +0400 Subject: [PATCH 12/41] chore(frontend): Standardize custom colors used throughout the app (#6833) --- .../features/controls/agent-status-bar.tsx | 2 +- .../conversation-panel/conversation-panel.tsx | 2 +- .../file-explorer/file-explorer-header.tsx | 2 +- .../features/file-explorer/file-explorer.tsx | 2 +- .../features/settings/brand-button.tsx | 4 ++-- .../features/settings/key-status-icon.tsx | 4 +--- .../features/settings/optional-tag.tsx | 2 +- .../settings/settings-dropdown-input.tsx | 4 ++-- .../features/settings/settings-input.tsx | 2 +- .../features/settings/settings-switch.tsx | 2 +- .../settings/styled-switch-component.tsx | 6 ++--- frontend/src/components/layout/beta-badge.tsx | 2 +- frontend/src/components/layout/container.tsx | 2 +- .../src/components/layout/count-badge.tsx | 2 +- frontend/src/components/layout/nav-tab.tsx | 4 ++-- .../src/components/shared/action-tooltip.tsx | 2 +- .../src/components/shared/hero-heading.tsx | 2 +- .../shared/modals/base-modal/base-modal.tsx | 2 +- .../components/shared/modals/modal-body.tsx | 2 +- .../modals/security/invariant/invariant.tsx | 4 ++-- .../shared/modals/settings/model-selector.tsx | 8 +++---- .../shared/modals/settings/settings-modal.tsx | 2 +- frontend/src/index.css | 22 ++++++++++--------- frontend/src/routes/_oh._index/route.tsx | 2 +- frontend/src/routes/_oh.app._index/route.tsx | 2 +- frontend/src/routes/_oh.app/route.tsx | 2 +- frontend/src/routes/_oh/route.tsx | 2 +- frontend/src/routes/account-settings.tsx | 4 ++-- frontend/src/routes/settings.tsx | 8 +++---- frontend/tailwind.config.js | 18 ++++++++------- 30 files changed, 63 insertions(+), 61 deletions(-) diff --git a/frontend/src/components/features/controls/agent-status-bar.tsx b/frontend/src/components/features/controls/agent-status-bar.tsx index ace99f8f4832..715db9a1b750 100644 --- a/frontend/src/components/features/controls/agent-status-bar.tsx +++ b/frontend/src/components/features/controls/agent-status-bar.tsx @@ -55,7 +55,7 @@ export function AgentStatusBar() { return (
-
+
diff --git a/frontend/src/components/features/conversation-panel/conversation-panel.tsx b/frontend/src/components/features/conversation-panel/conversation-panel.tsx index d91a70755ba1..5aeb69d7d318 100644 --- a/frontend/src/components/features/conversation-panel/conversation-panel.tsx +++ b/frontend/src/components/features/conversation-panel/conversation-panel.tsx @@ -73,7 +73,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
{isFetching && } diff --git a/frontend/src/components/features/file-explorer/file-explorer-header.tsx b/frontend/src/components/features/file-explorer/file-explorer-header.tsx index bf50cca87b6e..d28059514601 100644 --- a/frontend/src/components/features/file-explorer/file-explorer-header.tsx +++ b/frontend/src/components/features/file-explorer/file-explorer-header.tsx @@ -19,7 +19,7 @@ export function FileExplorerHeader({ return (
diff --git a/frontend/src/components/features/settings/brand-button.tsx b/frontend/src/components/features/settings/brand-button.tsx index b4d2dc24aa5f..4cbcbf5fc498 100644 --- a/frontend/src/components/features/settings/brand-button.tsx +++ b/frontend/src/components/features/settings/brand-button.tsx @@ -28,8 +28,8 @@ export function BrandButton({ onClick={onClick} className={cn( "w-fit p-2 rounded disabled:opacity-30 disabled:cursor-not-allowed", - variant === "primary" && "bg-[#C9B974] text-[#0D0F11]", - variant === "secondary" && "border border-[#C9B974] text-[#C9B974]", + variant === "primary" && "bg-primary text-[#0D0F11]", + variant === "secondary" && "border border-primary text-primary", className, )} > diff --git a/frontend/src/components/features/settings/key-status-icon.tsx b/frontend/src/components/features/settings/key-status-icon.tsx index ad4bd3cf8343..7fee524022b8 100644 --- a/frontend/src/components/features/settings/key-status-icon.tsx +++ b/frontend/src/components/features/settings/key-status-icon.tsx @@ -8,9 +8,7 @@ interface KeyStatusIconProps { export function KeyStatusIcon({ isSet }: KeyStatusIconProps) { return ( - + ); } diff --git a/frontend/src/components/features/settings/optional-tag.tsx b/frontend/src/components/features/settings/optional-tag.tsx index 3df207fc1b94..64c9cc913a99 100644 --- a/frontend/src/components/features/settings/optional-tag.tsx +++ b/frontend/src/components/features/settings/optional-tag.tsx @@ -1,3 +1,3 @@ export function OptionalTag() { - return (Optional); + return (Optional); } diff --git a/frontend/src/components/features/settings/settings-dropdown-input.tsx b/frontend/src/components/features/settings/settings-dropdown-input.tsx index 69385bf08f48..5bd89d7cd4d1 100644 --- a/frontend/src/components/features/settings/settings-dropdown-input.tsx +++ b/frontend/src/components/features/settings/settings-dropdown-input.tsx @@ -38,12 +38,12 @@ export function SettingsDropdownInput({ isDisabled={isDisabled} className="w-full" classNames={{ - popoverContent: "bg-[#454545] rounded-xl border border-[#717888]", + popoverContent: "bg-tertiary rounded-xl border border-[#717888]", }} inputProps={{ classNames: { inputWrapper: - "bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic", + "bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic", }, }} > diff --git a/frontend/src/components/features/settings/settings-input.tsx b/frontend/src/components/features/settings/settings-input.tsx index 5d737fd991dd..a9ba98911a50 100644 --- a/frontend/src/components/features/settings/settings-input.tsx +++ b/frontend/src/components/features/settings/settings-input.tsx @@ -44,7 +44,7 @@ export function SettingsInput({ defaultValue={defaultValue} placeholder={placeholder} className={cn( - "bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic placeholder:text-[#B7BDC2]", + "bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic placeholder:text-tertiary-alt", "disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed", )} /> diff --git a/frontend/src/components/features/settings/settings-switch.tsx b/frontend/src/components/features/settings/settings-switch.tsx index d1bfaff94935..21f3563038d4 100644 --- a/frontend/src/components/features/settings/settings-switch.tsx +++ b/frontend/src/components/features/settings/settings-switch.tsx @@ -40,7 +40,7 @@ export function SettingsSwitch({
{children} {isBeta && ( - + Beta )} diff --git a/frontend/src/components/features/settings/styled-switch-component.tsx b/frontend/src/components/features/settings/styled-switch-component.tsx index 36d9ffda6bfb..c00a55d18ce5 100644 --- a/frontend/src/components/features/settings/styled-switch-component.tsx +++ b/frontend/src/components/features/settings/styled-switch-component.tsx @@ -11,14 +11,14 @@ export function StyledSwitchComponent({
diff --git a/frontend/src/components/layout/beta-badge.tsx b/frontend/src/components/layout/beta-badge.tsx index 3a7155bb9be4..0ef82837eed3 100644 --- a/frontend/src/components/layout/beta-badge.tsx +++ b/frontend/src/components/layout/beta-badge.tsx @@ -4,7 +4,7 @@ import { I18nKey } from "#/i18n/declaration"; export function BetaBadge() { const { t } = useTranslation(); return ( - + {t(I18nKey.BADGE$BETA)} ); diff --git a/frontend/src/components/layout/container.tsx b/frontend/src/components/layout/container.tsx index d4d3a9ac6c34..490b9a37a226 100644 --- a/frontend/src/components/layout/container.tsx +++ b/frontend/src/components/layout/container.tsx @@ -23,7 +23,7 @@ export function Container({ return (
diff --git a/frontend/src/components/layout/count-badge.tsx b/frontend/src/components/layout/count-badge.tsx index 359ae3b71aee..96f0cf9639a9 100644 --- a/frontend/src/components/layout/count-badge.tsx +++ b/frontend/src/components/layout/count-badge.tsx @@ -1,6 +1,6 @@ export function CountBadge({ count }: { count: number }) { return ( - + {count} ); diff --git a/frontend/src/components/layout/nav-tab.tsx b/frontend/src/components/layout/nav-tab.tsx index a9f363e39532..678dfb327215 100644 --- a/frontend/src/components/layout/nav-tab.tsx +++ b/frontend/src/components/layout/nav-tab.tsx @@ -17,10 +17,10 @@ export function NavTab({ to, label, icon, isBeta }: NavTabProps) { to={to} className={({ isActive }) => cn( - "px-2 border-b border-r border-neutral-600 bg-root-primary flex-1", + "px-2 border-b border-r border-neutral-600 bg-base flex-1", "first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl last-of-type:border-r-0", "flex items-center gap-2", - isActive && "bg-root-secondary", + isActive && "bg-base-secondary", ) } > diff --git a/frontend/src/components/shared/action-tooltip.tsx b/frontend/src/components/shared/action-tooltip.tsx index 7863172a26fa..e287aa916d31 100644 --- a/frontend/src/components/shared/action-tooltip.tsx +++ b/frontend/src/components/shared/action-tooltip.tsx @@ -27,7 +27,7 @@ export function ActionTooltip({ type, onClick }: ActionTooltipProps) { ? t(I18nKey.ACTION$CONFIRM) : t(I18nKey.ACTION$REJECT) } - className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800" + className="bg-neutral-700 rounded-full p-1 hover:bg-base-secondary" onClick={onClick} > {type === "confirm" ? : } diff --git a/frontend/src/components/shared/hero-heading.tsx b/frontend/src/components/shared/hero-heading.tsx index 22dd254d865c..f7bca31b0355 100644 --- a/frontend/src/components/shared/hero-heading.tsx +++ b/frontend/src/components/shared/hero-heading.tsx @@ -18,7 +18,7 @@ export function HeroHeading() { rel="noopener noreferrer" target="_blank" href="https://docs.all-hands.dev/modules/usage/getting-started" - className="text-hyperlink underline underline-offset-[3px]" + className="text-white underline underline-offset-[3px]" > {t(I18nKey.LANDING$START_HELP_LINK)} diff --git a/frontend/src/components/shared/modals/base-modal/base-modal.tsx b/frontend/src/components/shared/modals/base-modal/base-modal.tsx index 4a95eb5eddcd..6d16ce450d51 100644 --- a/frontend/src/components/shared/modals/base-modal/base-modal.tsx +++ b/frontend/src/components/shared/modals/base-modal/base-modal.tsx @@ -43,7 +43,7 @@ export function BaseModal({ backdrop="blur" hideCloseButton size="sm" - className="bg-neutral-900 rounded-lg" + className="bg-base rounded-lg" > {(closeModal) => ( diff --git a/frontend/src/components/shared/modals/modal-body.tsx b/frontend/src/components/shared/modals/modal-body.tsx index 32e107b36772..9ed510b093ba 100644 --- a/frontend/src/components/shared/modals/modal-body.tsx +++ b/frontend/src/components/shared/modals/modal-body.tsx @@ -12,7 +12,7 @@ export function ModalBody({ testID, children, className }: ModalBodyProps) {
diff --git a/frontend/src/components/shared/modals/security/invariant/invariant.tsx b/frontend/src/components/shared/modals/security/invariant/invariant.tsx index 7e172c9d028f..ea03e8e159bf 100644 --- a/frontend/src/components/shared/modals/security/invariant/invariant.tsx +++ b/frontend/src/components/shared/modals/security/invariant/invariant.tsx @@ -247,7 +247,7 @@ function SecurityInvariant() { return (
-
+
{t(I18nKey.INVARIANT$INVARIANT_ANALYZER_LABEL)} @@ -285,7 +285,7 @@ function SecurityInvariant() {
-
+
{sections[activeSection as SectionType]}
diff --git a/frontend/src/components/shared/modals/settings/model-selector.tsx b/frontend/src/components/shared/modals/settings/model-selector.tsx index 811113844132..a316bb7c9fdf 100644 --- a/frontend/src/components/shared/modals/settings/model-selector.tsx +++ b/frontend/src/components/shared/modals/settings/model-selector.tsx @@ -84,12 +84,12 @@ export function ModelSelector({ defaultSelectedKey={selectedProvider ?? undefined} selectedKey={selectedProvider} classNames={{ - popoverContent: "bg-[#454545] rounded-xl border border-[#717888]", + popoverContent: "bg-tertiary rounded-xl border border-[#717888]", }} inputProps={{ classNames: { inputWrapper: - "bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic", + "bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic", }, }} > @@ -135,12 +135,12 @@ export function ModelSelector({ selectedKey={selectedModel} defaultSelectedKey={selectedModel ?? undefined} classNames={{ - popoverContent: "bg-[#454545] rounded-xl border border-[#717888]", + popoverContent: "bg-tertiary rounded-xl border border-[#717888]", }} inputProps={{ classNames: { inputWrapper: - "bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic", + "bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic", }, }} > diff --git a/frontend/src/components/shared/modals/settings/settings-modal.tsx b/frontend/src/components/shared/modals/settings/settings-modal.tsx index 723a86cec398..62257d8d2763 100644 --- a/frontend/src/components/shared/modals/settings/settings-modal.tsx +++ b/frontend/src/components/shared/modals/settings/settings-modal.tsx @@ -21,7 +21,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
{aiConfigOptions.error && (

{aiConfigOptions.error.message}

diff --git a/frontend/src/index.css b/frontend/src/index.css index b5814513e8bd..00a234baa46c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -4,18 +4,19 @@ --bg-input: #393939; --bg-workspace: #1f2228; --border: #3c3c4a; - --text-editor-base: #9099AC; - --text-editor-active:#C4CBDA; - --bg-editor-sidebar: #24272E; - --bg-editor-active: #31343D; - --border-editor-sidebar: #3C3C4A; - background-color: var(--neutral-900) !important; + --text-editor-base: #9099ac; + --text-editor-active: #c4cbda; + --bg-editor-sidebar: #24272e; + --bg-editor-active: #31343d; + --border-editor-sidebar: #3c3c4a; + background-color: var(--base) !important; --bg-neutral-muted: #afb8c133; } body { margin: 0; - font-family: -apple-system, "SF Pro", BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + font-family: + -apple-system, "SF Pro", BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; @@ -23,8 +24,8 @@ body { } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; + font-family: + source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } .markdown-body code { @@ -52,6 +53,7 @@ code { text-align: left; } -.markdown-body th, .markdown-body td { +.markdown-body th, +.markdown-body td { padding: 0.1rem 1rem; } diff --git a/frontend/src/routes/_oh._index/route.tsx b/frontend/src/routes/_oh._index/route.tsx index 23e4ccd08b3e..15d16e02d6d8 100644 --- a/frontend/src/routes/_oh._index/route.tsx +++ b/frontend/src/routes/_oh._index/route.tsx @@ -29,7 +29,7 @@ function Home() { const latestConversation = localStorage.getItem("latest_conversation_id"); return ( -
+
diff --git a/frontend/src/routes/_oh.app._index/route.tsx b/frontend/src/routes/_oh.app._index/route.tsx index 3f971213c8d3..521a3f6d927b 100644 --- a/frontend/src/routes/_oh.app._index/route.tsx +++ b/frontend/src/routes/_oh.app._index/route.tsx @@ -75,7 +75,7 @@ function FileViewer() { }; return ( -
+
{selectedPath && ( diff --git a/frontend/src/routes/_oh.app/route.tsx b/frontend/src/routes/_oh.app/route.tsx index c605927a0aa9..a52e63825fd1 100644 --- a/frontend/src/routes/_oh.app/route.tsx +++ b/frontend/src/routes/_oh.app/route.tsx @@ -127,7 +127,7 @@ function AppContent() { orientation={Orientation.HORIZONTAL} className="grow h-full min-h-0 min-w-0" initialSize={500} - firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-neutral-800" + firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-base-secondary" secondClassName="flex flex-col overflow-hidden" firstChild={} secondChild={ diff --git a/frontend/src/routes/_oh/route.tsx b/frontend/src/routes/_oh/route.tsx index 6d62dbcf1978..55ec6b783836 100644 --- a/frontend/src/routes/_oh/route.tsx +++ b/frontend/src/routes/_oh/route.tsx @@ -91,7 +91,7 @@ export default function MainApp() { return (
diff --git a/frontend/src/routes/account-settings.tsx b/frontend/src/routes/account-settings.tsx index 9d77959f30e5..05116104e304 100644 --- a/frontend/src/routes/account-settings.tsx +++ b/frontend/src/routes/account-settings.tsx @@ -388,7 +388,7 @@ function AccountSettings() {
-