Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5f31b643e | |||
| 5730db327a | |||
| 24a03738a8 | |||
| 3a093c13b8 | |||
| e367ced954 | |||
| 4c2c1bd8eb | |||
| 20b1f37fbd | |||
| 2bae39ef30 | |||
| 9f8bcf8729 | |||
| 137eec4dd1 |
@@ -2,8 +2,6 @@ This repository contains the code for OpenHands, an automated AI software engine
|
||||
(in the `openhands` directory) and React frontend (in the `frontend` directory).
|
||||
|
||||
## General Setup:
|
||||
To set up the entire repo, including frontend and backend, run `make build`.
|
||||
You don't need to do this unless the user asks you to, or if you're trying to run the entire application.
|
||||
|
||||
IMPORTANT: Before making any changes to the codebase, ALWAYS run `make install-pre-commit-hooks` to ensure pre-commit hooks are properly installed.
|
||||
|
||||
@@ -21,13 +19,91 @@ then re-run the command to ensure it passes. Common issues include:
|
||||
- Trailing whitespace
|
||||
- Missing newlines at end of files
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Environment Setup for Testing
|
||||
- Run `make build` to install all dependencies (only necessary for running tests):
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
**IMPORTANT**: When using `execute_bash` to run `make build` or similar long-running commands, set the `timeout` parameter to a high value (e.g., 600 seconds):
|
||||
```
|
||||
execute_bash(command="make build", timeout=600)
|
||||
```
|
||||
|
||||
#### Docker Installation
|
||||
**NOTE: Docker installation is ONLY required for running runtime tests with the Docker runtime.**
|
||||
|
||||
- Install Docker on Debian-based systems:
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
```
|
||||
- Start Docker daemon (in container environments without systemd):
|
||||
```bash
|
||||
sudo dockerd > /tmp/docker.log 2>&1 & sleep 5
|
||||
```
|
||||
- Verify Docker installation:
|
||||
```bash
|
||||
sudo docker run hello-world
|
||||
```
|
||||
|
||||
#### Development Environment Setup
|
||||
- Before running `make run`, ensure netcat is installed:
|
||||
```bash
|
||||
sudo apt-get install -y netcat-openbsd
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
- All unit tests are in `tests/unit/test_*.py`
|
||||
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
|
||||
- Write all tests with pytest
|
||||
|
||||
### Runtime Tests
|
||||
- Runtime tests are in `tests/runtime/test_*.py`
|
||||
- Run tests with different runtime implementations by setting the `TEST_RUNTIME` environment variable:
|
||||
```bash
|
||||
# Use Docker runtime (default)
|
||||
DEBUG=1 poetry run pytest -vvxss tests/runtime/test_bash.py
|
||||
|
||||
# Use CLI runtime (more reliable in some environments)
|
||||
DEBUG=1 TEST_RUNTIME=cli poetry run pytest -vvxss tests/runtime/test_bash.py
|
||||
|
||||
# Run a specific test
|
||||
DEBUG=1 TEST_RUNTIME=cli poetry run pytest -vvxss tests/runtime/test_bash.py::test_bash_server
|
||||
```
|
||||
- **IMPORTANT**: Runtime tests can take a long time to run, especially when building Docker images. Set a high timeout value:
|
||||
```
|
||||
execute_bash(command="DEBUG=1 poetry run pytest -vvxss tests/runtime/test_bash.py", timeout=600)
|
||||
```
|
||||
- The `DEBUG=1` flag enables more verbose logging
|
||||
- The `-vvxss` flags make the test output more verbose and stop after the first failure
|
||||
|
||||
### Debugging Docker Issues
|
||||
- Check Docker container status:
|
||||
```bash
|
||||
sudo docker ps -a
|
||||
```
|
||||
- View Docker logs:
|
||||
```bash
|
||||
sudo docker logs <container_id>
|
||||
```
|
||||
- Check Docker daemon logs:
|
||||
```bash
|
||||
sudo cat /tmp/docker.log | tail -n 100
|
||||
```
|
||||
- Check OpenHands logs:
|
||||
```bash
|
||||
cat logs/openhands_*.log | grep -i error | tail -n 20
|
||||
```
|
||||
|
||||
## Repository Structure
|
||||
Backend:
|
||||
- Located in the `openhands` directory
|
||||
- Testing:
|
||||
- All tests are in `tests/unit/test_*.py`
|
||||
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
|
||||
- Write all tests with pytest
|
||||
|
||||
Frontend:
|
||||
- Located in the `frontend` directory
|
||||
@@ -44,18 +120,19 @@ Frontend:
|
||||
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
|
||||
- Internationalization:
|
||||
- Generate i18n declaration file: `npm run make-i18n`
|
||||
- Data Fetching & Cache Management:
|
||||
- We use TanStack Query (fka React Query) for data fetching and cache management
|
||||
- Data Access Layer: API client methods are located in `frontend/src/api` and should never be called directly from UI components - they must always be wrapped with TanStack Query
|
||||
- Custom hooks are located in `frontend/src/hooks/query/` and `frontend/src/hooks/mutation/`
|
||||
- Query hooks should follow the pattern use[Resource] (e.g., `useConversationMicroagents`)
|
||||
- Mutation hooks should follow the pattern use[Action] (e.g., `useDeleteConversation`)
|
||||
- Architecture rule: UI components → TanStack Query hooks → Data Access Layer (`frontend/src/api`) → API endpoints
|
||||
|
||||
|
||||
## Template for Github Pull Request
|
||||
|
||||
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
|
||||
|
||||
## Runtime Architecture
|
||||
- OpenHands uses a Docker-based runtime for secure execution of agent actions
|
||||
- The runtime builds a custom Docker image based on a specified base image
|
||||
- The image includes OpenHands-specific code and the runtime client
|
||||
- The runtime client executes actions in the sandboxed environment and returns observations
|
||||
- More details in the [runtime architecture documentation](https://docs.all-hands.dev/usage/architecture/runtime)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
These details may or may not be useful for your current task.
|
||||
@@ -86,4 +163,4 @@ These details may or may not be useful for your current task.
|
||||
- Add the translation key to `frontend/src/i18n/declaration.ts`
|
||||
2. Add the setting to the backend:
|
||||
- Add the setting to the `Settings` model in `openhands/storage/data_models/settings.py`
|
||||
- Update any relevant backend code to apply the setting (e.g., in session creation)
|
||||
- Update any relevant backend code to apply the setting (e.g., in session creation)
|
||||
@@ -136,7 +136,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.42-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.41-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -18,17 +18,6 @@
|
||||
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
|
||||
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
|
||||
|
||||
<!-- Keep these links. Translations will automatically update with the README. -->
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=de">Deutsch</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=es">Español</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=fr">français</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ja">日本語</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ko">한국어</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=pt">Português</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ru">Русский</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=zh">中文</a>
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
@@ -62,17 +51,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-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.42
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.41
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-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.42
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.41
|
||||
```
|
||||
|
||||
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands!
|
||||
|
||||
@@ -11,12 +11,11 @@ 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.42-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.41-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
network_mode: host
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
|
||||
@@ -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.42-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.41-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:
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Setup
|
||||
|
||||
```
|
||||
npm install -g mint
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
yarn global add mint
|
||||
```
|
||||
|
||||
# Preview
|
||||
|
||||
```
|
||||
mint dev
|
||||
```
|
||||
@@ -34,8 +34,7 @@
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
"usage/cloud/github-installation",
|
||||
"usage/cloud/gitlab-installation",
|
||||
"usage/cloud/slack-installation"
|
||||
"usage/cloud/gitlab-installation"
|
||||
]
|
||||
},
|
||||
"usage/cloud/cloud-ui",
|
||||
@@ -49,62 +48,13 @@
|
||||
"usage/how-to/gui-mode",
|
||||
"usage/how-to/cli-mode",
|
||||
"usage/how-to/headless-mode",
|
||||
"usage/how-to/github-action",
|
||||
{
|
||||
"group": "Advanced Configuration",
|
||||
"pages": [
|
||||
{
|
||||
"group": "LLM Configuration",
|
||||
"pages": [
|
||||
"usage/llms/llms",
|
||||
{
|
||||
"group": "Providers",
|
||||
"pages": [
|
||||
"usage/llms/azure-llms",
|
||||
"usage/llms/google-llms",
|
||||
"usage/llms/groq",
|
||||
"usage/llms/local-llms",
|
||||
"usage/llms/litellm-proxy",
|
||||
"usage/llms/openai-llms",
|
||||
"usage/llms/openrouter"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Runtime Configuration",
|
||||
"pages": [
|
||||
"usage/runtimes/overview",
|
||||
{
|
||||
"group": "Providers",
|
||||
"pages": [
|
||||
"usage/runtimes/docker",
|
||||
"usage/runtimes/remote",
|
||||
"usage/runtimes/local",
|
||||
{
|
||||
"group": "Third-Party Providers",
|
||||
"pages": [
|
||||
"usage/runtimes/modal",
|
||||
"usage/runtimes/daytona",
|
||||
"usage/runtimes/runloop",
|
||||
"usage/runtimes/e2b"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage/configuration-options",
|
||||
"usage/how-to/custom-sandbox-guide",
|
||||
"usage/search-engine-setup",
|
||||
"usage/mcp"
|
||||
]
|
||||
}
|
||||
"usage/how-to/github-action"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Customization",
|
||||
"pages": [
|
||||
"usage/prompting/prompting-best-practices",
|
||||
"usage/prompting/repository",
|
||||
{
|
||||
"group": "Microagents",
|
||||
@@ -119,9 +69,53 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Tips and Tricks",
|
||||
"group": "Advanced Configuration",
|
||||
"pages": [
|
||||
"usage/prompting/prompting-best-practices"
|
||||
{
|
||||
"group": "LLM Configuration",
|
||||
"pages": [
|
||||
"usage/llms/llms",
|
||||
{
|
||||
"group": "Providers",
|
||||
"pages": [
|
||||
"usage/llms/azure-llms",
|
||||
"usage/llms/google-llms",
|
||||
"usage/llms/groq",
|
||||
"usage/llms/local-llms",
|
||||
"usage/llms/litellm-proxy",
|
||||
"usage/llms/openai-llms",
|
||||
"usage/llms/openrouter"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Runtime Configuration",
|
||||
"pages": [
|
||||
"usage/runtimes/overview",
|
||||
{
|
||||
"group": "Providers",
|
||||
"pages": [
|
||||
"usage/runtimes/docker",
|
||||
"usage/runtimes/remote",
|
||||
"usage/runtimes/local",
|
||||
{
|
||||
"group": "Third-Party Providers",
|
||||
"pages": [
|
||||
"usage/runtimes/modal",
|
||||
"usage/runtimes/daytona",
|
||||
"usage/runtimes/runloop",
|
||||
"usage/runtimes/e2b"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage/configuration-options",
|
||||
"usage/how-to/custom-sandbox-guide",
|
||||
"usage/search-engine-setup",
|
||||
"usage/mcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 542 KiB |
@@ -19,12 +19,6 @@ appropriate repository and branch you'd like OpenHands to work on. Then click on
|
||||
|
||||

|
||||
|
||||
## Using Tokens with Reduced Scopes
|
||||
|
||||
OpenHands requests an API-scoped token during OAuth authentication. By default, this token is provided to the agent.
|
||||
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default token assigned to the agent.
|
||||
While the high-permission API token is still requested and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
title: Slack Integration - Coming soon...
|
||||
description: This guide walks you through installing the OpenHands Slack app.
|
||||
---
|
||||
|
||||
<Warning>This integration is not live yet, but will be available soon.</Warning>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You are a slack workspace admin
|
||||
- Access to OpenHands Cloud
|
||||
|
||||
## Installation Steps
|
||||
|
||||
1. Log in to [OpenHands Cloud](https://app.all-hands.dev)
|
||||
2. Click the button below to OpenHands Slack App <a target="_blank" href="https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,chat:write,users:read,channels:history,groups:history,mpim:history,im:history&user_scope=channels:history,groups:history,im:history,mpim:history"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
|
||||
3. In the top right corner, select the workspace to install the OpenHands Slack app.
|
||||
4. Review permissions and click allow
|
||||
|
||||
## Working With the Slack App
|
||||
|
||||
To start a new conversation, you can mention `@openhands` in a new message or a thread inside any Slack channel.
|
||||
|
||||
Once a conversation is started, all thread messages underneath it will be follow-up messages to OpenHands.
|
||||
|
||||
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message. You must be the user who started the conversation.
|
||||
|
||||
## Example conversation
|
||||
|
||||
### Start a new conversation, and select repo
|
||||
|
||||
Conversation is started by mentioning `@openhands`.
|
||||
|
||||

|
||||
|
||||
### See agent response and send follow up messages
|
||||
|
||||
Initial request is followed up by mentioning `@openhands` in a thread reply.
|
||||
|
||||

|
||||
|
||||
## Pro tip
|
||||
|
||||
You can mention a repo name when starting a new conversation in the following formats
|
||||
|
||||
1. "My-Repo" repo (e.g `@openhands in the openhands repo ...`)
|
||||
2. "All-Hands-AI/OpenHands" (e.g `@openhands in All-Hands-AI/OpenHands ...`)
|
||||
|
||||
The repo match is case insensitive. If a repo name match is made, it will kick off the conversation.
|
||||
If the repo name partially matches against, multiple repos, you'll be asked to select a repo from the filtered list.
|
||||
|
||||

|
||||
@@ -46,7 +46,7 @@ poetry run python -m openhands.cli.main
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -55,7 +55,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.42 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
|
||||
python -m openhands.cli.main --override-cli-mode true
|
||||
```
|
||||
|
||||
|
||||
@@ -100,11 +100,6 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
|
||||
- In the Settings page, navigate to the `Git` tab.
|
||||
- Paste your token in the `GitLab Token` field.
|
||||
- Click `Save Changes` to apply the changes.
|
||||
|
||||
3. **(Optional): Restrict agent permissions**
|
||||
- Create another PAT using Step 1 and exclude `api` scope .
|
||||
- In the Settings page, in the `Secrets` tab, create a new secret `GITLAB_TOKEN` and paste your lower scope token.
|
||||
- OpenHands will use the higher scope token, and the agent will use the lower scope token
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
@@ -137,20 +132,6 @@ toggle `Advanced` options to access additional settings.
|
||||
For an overview of the key features available inside a conversation, please refer to the [Key Features](/usage/key-features)
|
||||
section of the documentation.
|
||||
|
||||
### Status Indicator
|
||||
|
||||
The status indicator located in the bottom left of the screen will cycle through a number of states as a new conversation
|
||||
is loaded. Typically these include:
|
||||
|
||||
* `Disconnected` : The frontend is not connected to any conversation
|
||||
* `Connecting` : The frontend is connecting a websocket to a conversation.
|
||||
* `Building Runtime...` : The server is building a runtime. This is typically in development mode only while building a docker image.
|
||||
* `Starting Runtime...` : The server is starting a new runtime instance - probably a new docker container or remote runtime.
|
||||
* `Initializing Agent...` : The server is starting the agent loop. (This step does not appear at present with Nested runtimes)
|
||||
* `Setting up workspace...` : Usually this means a `git clone ...` operation.
|
||||
* `Setting up git hooks` : Setting up the git pre commit hooks for the workspace.
|
||||
* `Agent is awaiting user input...` : Ready to go!
|
||||
|
||||
## Tips for Effective Use
|
||||
|
||||
- Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
|
||||
|
||||
@@ -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.42-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -42,7 +42,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.42 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -14,28 +14,23 @@ recommendations for model selection. Our latest benchmarking results can be foun
|
||||
|
||||
Based on these findings and community feedback, these are the latest models that have been verified to work reasonably well with OpenHands:
|
||||
|
||||
### Cloud / API-Based Models
|
||||
|
||||
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
|
||||
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
|
||||
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
|
||||
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
|
||||
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) -- available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
|
||||
|
||||
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
|
||||
to help others using the same provider!
|
||||
|
||||
For a full list of the providers and models available, please consult the
|
||||
[litellm documentation](https://docs.litellm.ai/docs/providers).
|
||||
|
||||
<Warning>
|
||||
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
|
||||
limits and monitor usage.
|
||||
</Warning>
|
||||
|
||||
### Local / Self-Hosted Models
|
||||
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
|
||||
to help others using the same provider!
|
||||
|
||||
- [mistralai/devstral-small](https://www.all-hands.dev/blog/devstral-a-new-state-of-the-art-open-model-for-coding-agents) (20 May 2025) -- also available through [OpenRouter](https://openrouter.ai/mistralai/devstral-small:free)
|
||||
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) (31 March 2025) -- also available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
|
||||
For a full list of the providers and models available, please consult the
|
||||
[litellm documentation](https://docs.litellm.ai/docs/providers).
|
||||
|
||||
<Note>
|
||||
Most current local and open source models are not as powerful. When using such models, you may see long
|
||||
|
||||
@@ -54,25 +54,25 @@ Check [the installation guide](/usage/local-setup) to make sure you have all the
|
||||
export LMSTUDIO_MODEL_NAME="imported-models/uncategorized/devstralq4_k_m.gguf" # <- Replace this with the model name you copied from LMStudio
|
||||
export LMSTUDIO_URL="http://host.docker.internal:1234" # <- Replace this with the port from LMStudio
|
||||
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
|
||||
|
||||
mkdir -p ~/.openhands-state && echo '{"language":"en","agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":false,"llm_model":"lm_studio/'$LMSTUDIO_MODEL_NAME'","llm_api_key":"dummy","llm_base_url":"'$LMSTUDIO_URL/v1'","remote_runtime_resource_factor":null,"github_token":null,"enable_default_condenser":true,"user_consents_to_analytics":true}' > ~/.openhands-state/settings.json
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-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.42
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.41
|
||||
```
|
||||
|
||||
Once your server is running -- you can visit `http://localhost:3000` in your browser to use OpenHands with local Devstral model:
|
||||
```
|
||||
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.42
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.41
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
|
||||
@@ -62,17 +62,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
### Start the App
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-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.42
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.41
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
@@ -117,24 +117,10 @@ OpenHands requires an API key to access most language models. Here's how to get
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Local LLM (e.g. LM Studio, llama.cpp, Ollama)">
|
||||
|
||||
If your local LLM server isn’t behind an authentication proxy, you can enter any value as the API key (e.g. `local-key`, `test123`) — it won’t be used.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
|
||||
Consider setting usage limits to control costs.
|
||||
|
||||
#### Using a Local LLM
|
||||
|
||||
<Note>
|
||||
Effective use of local models for agent tasks requires capable hardware, along with models specifically tuned for instruction-following and agent-style behavior.
|
||||
</Note>
|
||||
|
||||
To run OpenHands with a locally hosted language model instead of a cloud provider, see the [Local LLMs guide](/usage/llms/local-llms) for setup instructions.
|
||||
|
||||
#### Setting Up Search Engine
|
||||
|
||||
OpenHands can be configured to use a search engine to allow the agent to search the web for information when needed.
|
||||
|
||||
@@ -478,7 +478,7 @@ describe("ConversationCard", () => {
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
conversationStatus="RUNNING"
|
||||
status="RUNNING"
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ describe("ConversationPanel", () => {
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -61,7 +60,6 @@ describe("ConversationPanel", () => {
|
||||
last_updated_at: "2021-10-02T12:00:00Z",
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -74,7 +72,6 @@ describe("ConversationPanel", () => {
|
||||
last_updated_at: "2021-10-03T12:00:00Z",
|
||||
created_at: "2021-10-03T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -161,7 +158,6 @@ describe("ConversationPanel", () => {
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -174,7 +170,6 @@ describe("ConversationPanel", () => {
|
||||
last_updated_at: "2021-10-02T12:00:00Z",
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -187,7 +182,6 @@ describe("ConversationPanel", () => {
|
||||
last_updated_at: "2021-10-03T12:00:00Z",
|
||||
created_at: "2021-10-03T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
|
||||
@@ -65,7 +65,6 @@ describe("WsClientProvider", () => {
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "RUNNING" as const,
|
||||
runtime_status: "STATUS$READY",
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
}}},
|
||||
|
||||
@@ -334,7 +334,10 @@ describe("Settings 404", () => {
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
|
||||
// small hack to wait for the modal to not appear
|
||||
await expect(
|
||||
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { heroui } from "@heroui/react";
|
||||
|
||||
export default heroui({
|
||||
defaultTheme: "dark",
|
||||
layout: {
|
||||
radius: {
|
||||
small: "5px",
|
||||
large: "20px",
|
||||
},
|
||||
},
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
primary: "#4465DB",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,39 +1,37 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.42.0",
|
||||
"version": "0.41.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.0-beta.7",
|
||||
"@heroui/react": "2.7.8",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.6.2",
|
||||
"@react-router/serve": "^7.6.2",
|
||||
"@react-router/node": "^7.6.1",
|
||||
"@react-router/serve": "^7.6.1",
|
||||
"@react-types/shared": "^3.29.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@tanstack/react-query": "^5.80.7",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"@stripe/stripe-js": "^7.3.0",
|
||||
"@tanstack/react-query": "^5.77.2",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.17.3",
|
||||
"framer-motion": "^12.14.0",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.28",
|
||||
"jose": "^6.0.11",
|
||||
"lucide-react": "^0.514.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.251.0",
|
||||
"posthog-js": "^1.245.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -42,15 +40,15 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.6.2",
|
||||
"react-router": "^7.6.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"vite": "^6.3.5",
|
||||
"web-vitals": "^5.0.3",
|
||||
"web-vitals": "^5.0.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -84,23 +82,23 @@
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.53.0",
|
||||
"@react-router/dev": "^7.6.2",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@react-router/dev": "^7.6.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.78.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/react": "^19.1.5",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^3.2.3",
|
||||
"@vitest/coverage-v8": "^3.1.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -115,11 +113,12 @@
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"msw": "^2.6.6",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"stripe": "^18.2.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"stripe": "^18.1.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,9 +117,6 @@ const EXCLUDED_TECHNICAL_STRINGS = [
|
||||
"edit-secret-form", // Test ID for secret form
|
||||
"search-api-key-input", // Input name for search API key
|
||||
"noopener,noreferrer", // Options for window.open
|
||||
"STATUS$READY",
|
||||
"STATUS$STOPPED",
|
||||
"STATUS$ERROR",
|
||||
];
|
||||
|
||||
function isExcludedTechnicalString(str) {
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
GetTrajectoryResponse,
|
||||
GitChangeDiff,
|
||||
GitChange,
|
||||
GetMicroagentsResponse,
|
||||
GetMicroagentPromptResponse,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
|
||||
@@ -395,35 +393,6 @@ class OpenHands {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available microagents associated with a conversation
|
||||
* @param conversationId The ID of the conversation
|
||||
* @returns The available microagents associated with the conversation
|
||||
*/
|
||||
static async getMicroagents(
|
||||
conversationId: string,
|
||||
): Promise<GetMicroagentsResponse> {
|
||||
const url = `${this.getConversationUrl(conversationId)}/microagents`;
|
||||
const { data } = await openHands.get<GetMicroagentsResponse>(url, {
|
||||
headers: this.getConversationHeaders(),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getMicroagentPrompt(
|
||||
conversationId: string,
|
||||
eventId: number,
|
||||
): Promise<string> {
|
||||
const { data } = await openHands.get<GetMicroagentPromptResponse>(
|
||||
`/api/conversations/${conversationId}/remember_prompt`,
|
||||
{
|
||||
params: { event_id: eventId },
|
||||
},
|
||||
);
|
||||
|
||||
return data.prompt;
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenHands;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { RuntimeStatus } from "#/types/runtime-status";
|
||||
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
@@ -81,8 +80,7 @@ export interface Conversation {
|
||||
git_provider: string | null;
|
||||
last_updated_at: string;
|
||||
created_at: string;
|
||||
status: ConversationStatus;
|
||||
runtime_status: RuntimeStatus | null;
|
||||
status: ProjectStatus;
|
||||
trigger?: ConversationTrigger;
|
||||
url: string | null;
|
||||
session_api_key: string | null;
|
||||
@@ -104,24 +102,3 @@ export interface GitChangeDiff {
|
||||
modified: string;
|
||||
original: string;
|
||||
}
|
||||
|
||||
export interface InputMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Microagent {
|
||||
name: string;
|
||||
type: "repo" | "knowledge";
|
||||
content: string;
|
||||
triggers: string[];
|
||||
}
|
||||
|
||||
export interface GetMicroagentsResponse {
|
||||
microagents: Microagent[];
|
||||
}
|
||||
|
||||
export interface GetMicroagentPromptResponse {
|
||||
status: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
export enum IndicatorColor {
|
||||
BLUE = "bg-blue-500",
|
||||
GREEN = "bg-green-500",
|
||||
ORANGE = "bg-orange-500",
|
||||
YELLOW = "bg-yellow-500",
|
||||
RED = "bg-red-500",
|
||||
DARK_ORANGE = "bg-orange-800",
|
||||
}
|
||||
|
||||
export const AGENT_STATUS_MAP: {
|
||||
[k: string]: { message: string; indicator: IndicatorColor };
|
||||
} = {
|
||||
[AgentState.INIT]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_INIT_MESSAGE,
|
||||
indicator: IndicatorColor.BLUE,
|
||||
},
|
||||
[AgentState.RUNNING]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_RUNNING_MESSAGE,
|
||||
indicator: IndicatorColor.GREEN,
|
||||
},
|
||||
[AgentState.AWAITING_USER_INPUT]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE,
|
||||
indicator: IndicatorColor.BLUE,
|
||||
},
|
||||
[AgentState.PAUSED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE,
|
||||
indicator: IndicatorColor.YELLOW,
|
||||
},
|
||||
[AgentState.LOADING]: {
|
||||
message: I18nKey.CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE,
|
||||
indicator: IndicatorColor.DARK_ORANGE,
|
||||
},
|
||||
[AgentState.STOPPED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_STOPPED_MESSAGE,
|
||||
indicator: IndicatorColor.RED,
|
||||
},
|
||||
[AgentState.FINISHED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_FINISHED_MESSAGE,
|
||||
indicator: IndicatorColor.GREEN,
|
||||
},
|
||||
[AgentState.REJECTED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_REJECTED_MESSAGE,
|
||||
indicator: IndicatorColor.YELLOW,
|
||||
},
|
||||
[AgentState.ERROR]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE,
|
||||
indicator: IndicatorColor.RED,
|
||||
},
|
||||
[AgentState.AWAITING_USER_CONFIRMATION]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE,
|
||||
indicator: IndicatorColor.ORANGE,
|
||||
},
|
||||
[AgentState.USER_CONFIRMED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE,
|
||||
indicator: IndicatorColor.GREEN,
|
||||
},
|
||||
[AgentState.USER_REJECTED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE,
|
||||
indicator: IndicatorColor.RED,
|
||||
},
|
||||
[AgentState.RATE_LIMITED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE,
|
||||
indicator: IndicatorColor.YELLOW,
|
||||
},
|
||||
};
|
||||
@@ -132,7 +132,7 @@ export function ChatInput({
|
||||
maxRows={maxRows}
|
||||
data-dragging-over={isDraggingOver}
|
||||
className={cn(
|
||||
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none outline-hidden ring-0",
|
||||
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none outline-none ring-0",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
isDraggingOver
|
||||
? "bg-neutral-600/50 rounded-lg px-2"
|
||||
|
||||
@@ -114,7 +114,7 @@ export function ExpandableMessage({
|
||||
{t(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS)}
|
||||
</div>
|
||||
<Link
|
||||
className="mt-2 mb-2 w-full h-10 rounded-sm flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
|
||||
className="mt-2 mb-2 w-full h-10 rounded flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
|
||||
to="/settings/billing"
|
||||
>
|
||||
{t(I18nKey.BILLING$CLICK_TO_TOP_UP)}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { showErrorToast } from "#/utils/error-handler";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import {
|
||||
AGENT_STATUS_MAP,
|
||||
IndicatorColor,
|
||||
} from "../../agent-status-map.constant";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useNotification } from "#/hooks/useNotification";
|
||||
import { browserTab } from "#/utils/browser-tab";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { getIndicatorColor, getStatusCode } from "#/utils/status";
|
||||
|
||||
const notificationStates = [
|
||||
AgentState.AWAITING_USER_INPUT,
|
||||
@@ -20,60 +24,38 @@ export function AgentStatusBar() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curStatusMessage } = useSelector((state: RootState) => state.status);
|
||||
const { webSocketStatus } = useWsClient();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const indicatorColor = getIndicatorColor(
|
||||
webSocketStatus,
|
||||
conversation?.status || null,
|
||||
conversation?.runtime_status || null,
|
||||
curAgentState,
|
||||
);
|
||||
const statusCode = getStatusCode(
|
||||
curStatusMessage,
|
||||
webSocketStatus,
|
||||
conversation?.status || null,
|
||||
conversation?.runtime_status || null,
|
||||
curAgentState,
|
||||
);
|
||||
const { status } = useWsClient();
|
||||
const { notify } = useNotification();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
// Show error toast if required
|
||||
React.useEffect(() => {
|
||||
if (curStatusMessage?.type !== "error") {
|
||||
return;
|
||||
}
|
||||
const [statusMessage, setStatusMessage] = React.useState<string>("");
|
||||
|
||||
const updateStatusMessage = () => {
|
||||
let message = curStatusMessage.message || "";
|
||||
if (curStatusMessage?.id) {
|
||||
const id = curStatusMessage.id.trim();
|
||||
if (id === "STATUS$READY") {
|
||||
message = "awaiting_user_input";
|
||||
}
|
||||
if (i18n.exists(id)) {
|
||||
message = t(curStatusMessage.id.trim()) || message;
|
||||
}
|
||||
}
|
||||
showErrorToast({
|
||||
message,
|
||||
source: "agent-status",
|
||||
metadata: { ...curStatusMessage },
|
||||
});
|
||||
}, [curStatusMessage.id]);
|
||||
|
||||
// Handle notify
|
||||
React.useEffect(() => {
|
||||
if (notificationStates.includes(curAgentState)) {
|
||||
const message = t(statusCode);
|
||||
notify(message, {
|
||||
body: t(`Agent state changed to ${curAgentState}`),
|
||||
playSound: true,
|
||||
if (curStatusMessage?.type === "error") {
|
||||
showErrorToast({
|
||||
message,
|
||||
source: "agent-status",
|
||||
metadata: { ...curStatusMessage },
|
||||
});
|
||||
|
||||
// Update browser tab if window exists and is not focused
|
||||
if (typeof document !== "undefined" && !document.hasFocus()) {
|
||||
browserTab.startNotification(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [curAgentState, statusCode]);
|
||||
if (message.trim()) {
|
||||
setStatusMessage(message);
|
||||
} else {
|
||||
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
updateStatusMessage();
|
||||
}, [curStatusMessage.id]);
|
||||
|
||||
// Handle window focus/blur
|
||||
React.useEffect(() => {
|
||||
@@ -90,13 +72,45 @@ export function AgentStatusBar() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [indicatorColor, setIndicatorColor] = React.useState<string>(
|
||||
AGENT_STATUS_MAP[curAgentState].indicator,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (conversation?.status === "CONNECTING") {
|
||||
setStatusMessage(t(I18nKey.STATUS$CONNECTING_TO_RUNTIME));
|
||||
setIndicatorColor(IndicatorColor.YELLOW);
|
||||
} else if (conversation?.status === "STARTING") {
|
||||
setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME));
|
||||
setIndicatorColor(IndicatorColor.RED);
|
||||
} else if (status === "DISCONNECTED") {
|
||||
setStatusMessage(t(I18nKey.STATUS$WEBSOCKET_CLOSED));
|
||||
setIndicatorColor(IndicatorColor.RED);
|
||||
} else {
|
||||
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
||||
setIndicatorColor(AGENT_STATUS_MAP[curAgentState].indicator);
|
||||
if (notificationStates.includes(curAgentState)) {
|
||||
const message = t(AGENT_STATUS_MAP[curAgentState].message);
|
||||
notify(t(AGENT_STATUS_MAP[curAgentState].message), {
|
||||
body: t(`Agent state changed to ${curAgentState}`),
|
||||
playSound: true,
|
||||
});
|
||||
|
||||
// Update browser tab if window exists and is not focused
|
||||
if (typeof document !== "undefined" && !document.hasFocus()) {
|
||||
browserTab.startNotification(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [curAgentState, status, notify, t, conversation?.status]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-center bg-base-secondary px-2 py-1 text-gray-400 rounded-[100px] text-sm gap-[6px]">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full animate-pulse ${indicatorColor}`}
|
||||
/>
|
||||
<span className="text-sm text-stone-400">{t(statusCode)}</span>
|
||||
<span className="text-sm text-stone-400">{t(statusMessage)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
|
||||
title={conversation?.title ?? ""}
|
||||
lastUpdatedAt={conversation?.created_at ?? ""}
|
||||
selectedRepository={conversation?.selected_repository ?? null}
|
||||
conversationStatus={conversation?.status}
|
||||
status={conversation?.status}
|
||||
conversationId={conversation?.conversation_id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ContextMenu } from "../context-menu/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ConversationCardContextMenuProps {
|
||||
onClose: () => void;
|
||||
@@ -11,7 +9,6 @@ interface ConversationCardContextMenuProps {
|
||||
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowAgentTools?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowMicroagents?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
position?: "top" | "bottom";
|
||||
}
|
||||
@@ -22,11 +19,9 @@ export function ConversationCardContextMenu({
|
||||
onEdit,
|
||||
onDisplayCost,
|
||||
onShowAgentTools,
|
||||
onShowMicroagents,
|
||||
onDownloadViaVSCode,
|
||||
position = "bottom",
|
||||
}: ConversationCardContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
|
||||
return (
|
||||
@@ -73,14 +68,6 @@ export function ConversationCardContextMenu({
|
||||
Show Agent Tools & Metadata
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onShowMicroagents && (
|
||||
<ContextMenuListItem
|
||||
testId="show-microagents-button"
|
||||
onClick={onShowMicroagents}
|
||||
>
|
||||
{t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import posthog from "posthog-js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import { ConversationStateIndicator } from "./conversation-state-indicator";
|
||||
import {
|
||||
ProjectStatus,
|
||||
ConversationStateIndicator,
|
||||
} from "./conversation-state-indicator";
|
||||
import { EllipsisButton } from "./ellipsis-button";
|
||||
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
|
||||
import { SystemMessageModal } from "./system-message-modal";
|
||||
import { MicroagentsModal } from "./microagents-modal";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
|
||||
import { RootState } from "#/store";
|
||||
@@ -17,7 +19,6 @@ import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { isSystemMessage } from "#/types/core/guards";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
|
||||
interface ConversationCardProps {
|
||||
onClick?: () => void;
|
||||
@@ -29,7 +30,7 @@ interface ConversationCardProps {
|
||||
selectedRepository: string | null;
|
||||
lastUpdatedAt: string; // ISO 8601
|
||||
createdAt?: string; // ISO 8601
|
||||
conversationStatus?: ConversationStatus;
|
||||
status?: ProjectStatus;
|
||||
variant?: "compact" | "default";
|
||||
conversationId?: string; // Optional conversation ID for VS Code URL
|
||||
}
|
||||
@@ -48,7 +49,7 @@ export function ConversationCard({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
lastUpdatedAt,
|
||||
createdAt,
|
||||
conversationStatus = "STOPPED",
|
||||
status = "STOPPED",
|
||||
variant = "default",
|
||||
conversationId,
|
||||
}: ConversationCardProps) {
|
||||
@@ -58,8 +59,6 @@ export function ConversationCard({
|
||||
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
|
||||
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
|
||||
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
|
||||
const [microagentsModalVisible, setMicroagentsModalVisible] =
|
||||
React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const systemMessage = parsedEvents.find(isSystemMessage);
|
||||
@@ -143,13 +142,6 @@ export function ConversationCard({
|
||||
setSystemModalVisible(true);
|
||||
};
|
||||
|
||||
const handleShowMicroagents = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
setMicroagentsModalVisible(true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (titleMode === "edit") {
|
||||
inputRef.current?.focus();
|
||||
@@ -204,9 +196,7 @@ export function ConversationCard({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<ConversationStateIndicator
|
||||
conversationStatus={conversationStatus}
|
||||
/>
|
||||
<ConversationStateIndicator status={status} />
|
||||
{hasContextMenu && (
|
||||
<div className="pl-2">
|
||||
<EllipsisButton
|
||||
@@ -235,11 +225,6 @@ export function ConversationCard({
|
||||
? handleShowAgentTools
|
||||
: undefined
|
||||
}
|
||||
onShowMicroagents={
|
||||
showOptions && conversationId
|
||||
? handleShowMicroagents
|
||||
: undefined
|
||||
}
|
||||
position={variant === "compact" ? "top" : "bottom"}
|
||||
/>
|
||||
)}
|
||||
@@ -382,13 +367,6 @@ export function ConversationCard({
|
||||
onClose={() => setSystemModalVisible(false)}
|
||||
systemMessage={systemMessage ? systemMessage.args : null}
|
||||
/>
|
||||
|
||||
{microagentsModalVisible && (
|
||||
<MicroagentsModal
|
||||
onClose={() => setMicroagentsModalVisible(false)}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
selectedRepository={project.selected_repository}
|
||||
lastUpdatedAt={project.last_updated_at}
|
||||
createdAt={project.created_at}
|
||||
conversationStatus={project.status}
|
||||
status={project.status}
|
||||
conversationId={project.conversation_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import ColdIcon from "./state-indicators/cold.svg?react";
|
||||
import RunningIcon from "./state-indicators/running.svg?react";
|
||||
import StartingIcon from "./state-indicators/starting.svg?react";
|
||||
import StoppedIcon from "./state-indicators/stopped.svg?react";
|
||||
|
||||
type SVGIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
export type ProjectStatus =
|
||||
| "RUNNING"
|
||||
| "STOPPED"
|
||||
| "STARTING"
|
||||
| "CONNECTING"
|
||||
| "CONNECTED"
|
||||
| "DISCONNECTED";
|
||||
|
||||
const CONVERSATION_STATUS_INDICATORS: Record<ConversationStatus, SVGIcon> = {
|
||||
STOPPED: StoppedIcon,
|
||||
type ProjectStatusWithIcon = Exclude<
|
||||
ProjectStatus,
|
||||
"CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||
>;
|
||||
|
||||
const INDICATORS: Record<ProjectStatusWithIcon, SVGIcon> = {
|
||||
STOPPED: ColdIcon,
|
||||
RUNNING: RunningIcon,
|
||||
STARTING: StartingIcon,
|
||||
STARTING: ColdIcon,
|
||||
};
|
||||
|
||||
interface ConversationStateIndicatorProps {
|
||||
conversationStatus: ConversationStatus;
|
||||
status: ProjectStatus;
|
||||
}
|
||||
|
||||
export function ConversationStateIndicator({
|
||||
conversationStatus,
|
||||
status,
|
||||
}: ConversationStateIndicatorProps) {
|
||||
const StateIcon = CONVERSATION_STATUS_INDICATORS[conversationStatus];
|
||||
// @ts-expect-error - Type 'ProjectStatus' is not assignable to type 'ProjectStatusWithIcon'.
|
||||
const StateIcon = INDICATORS[status];
|
||||
|
||||
return (
|
||||
<div data-testid={`${conversationStatus}-indicator`}>
|
||||
<div data-testid={`${status}-indicator`}>
|
||||
<StateIcon />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationMicroagents } from "#/hooks/query/use-conversation-microagents";
|
||||
|
||||
interface MicroagentsModalProps {
|
||||
onClose: () => void;
|
||||
conversationId: string | undefined;
|
||||
}
|
||||
|
||||
export function MicroagentsModal({
|
||||
onClose,
|
||||
conversationId,
|
||||
}: MicroagentsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expandedAgents, setExpandedAgents] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
|
||||
const {
|
||||
data: microagents,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useConversationMicroagents({
|
||||
conversationId,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const toggleAgent = (agentName: string) => {
|
||||
setExpandedAgents((prev) => ({
|
||||
...prev,
|
||||
[agentName]: !prev[agentName],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
<ModalBody
|
||||
width="medium"
|
||||
className="max-h-[80vh] flex flex-col items-start"
|
||||
testID="microagents-modal"
|
||||
>
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
|
||||
</div>
|
||||
|
||||
<div className="w-full h-[60vh] overflow-auto rounded-md">
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading &&
|
||||
(isError || !microagents || microagents.length === 0) && (
|
||||
<div className="flex items-center justify-center h-full p-4">
|
||||
<p className="text-gray-400">
|
||||
{isError
|
||||
? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
|
||||
: t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && microagents && microagents.length > 0 && (
|
||||
<div className="p-2 space-y-3">
|
||||
{microagents.map((agent) => {
|
||||
const isExpanded = expandedAgents[agent.name] || false;
|
||||
|
||||
return (
|
||||
<div key={agent.name} className="rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAgent(agent.name)}
|
||||
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-bold text-gray-100">
|
||||
{agent.name}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
|
||||
{agent.type === "repo" ? "Repository" : "Knowledge"}
|
||||
</span>
|
||||
<span className="text-gray-300">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={18} />
|
||||
) : (
|
||||
<ChevronRight size={18} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-2 pb-3 pt-1">
|
||||
{agent.triggers && agent.triggers.length > 0 && (
|
||||
<div className="mt-2 mb-3">
|
||||
<h4 className="text-sm font-semibold text-gray-300 mb-2">
|
||||
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.triggers.map((trigger) => (
|
||||
<span
|
||||
key={trigger}
|
||||
className="px-2 py-1 text-xs rounded-full bg-blue-900"
|
||||
>
|
||||
{trigger}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2">
|
||||
<h4 className="text-sm font-semibold text-gray-300 mb-2">
|
||||
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
|
||||
</h4>
|
||||
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
|
||||
{agent.content ||
|
||||
t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 904 B After Width: | Height: | Size: 904 B |
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 968 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 16.8599C13.4183 16.8599 17 13.2781 17 8.85986C17 4.44159 13.4183 0.859863 9 0.859863C4.58172 0.859863 1 4.44159 1 8.85986C1 13.2781 4.58172 16.8599 9 16.8599Z" fill="#779FD4"/>
|
||||
<path d="M4.61035 8.43014L7.86035 12.0301L13.3904 6.64014" stroke="#231F20" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.76039 6.99002C8.478 6.99002 9.87039 5.59763 9.87039 3.88002C9.87039 2.16241 8.478 0.77002 6.76039 0.77002C5.04279 0.77002 3.65039 2.16241 3.65039 3.88002C3.65039 5.59763 5.04279 6.99002 6.76039 6.99002Z" fill="#FFE165"/>
|
||||
<path d="M1.0802 17.0799C1.0802 17.0799 0.610196 11.5499 3.0102 9.67992C4.7902 8.29992 7.3302 9.44992 9.7802 7.95992C11.5802 6.86992 13.6102 4.10992 14.5202 2.49992C14.9302 1.77992 15.9102 1.62992 16.6102 2.05992C17.3802 2.51992 17.6102 3.53992 17.1102 4.28992C16.2302 5.58992 14.1802 8.85992 13.1202 10.3699C10.7602 13.7599 11.4302 17.0799 11.4302 17.0799H1.0702H1.0802Z" fill="#FFE165"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 726 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.87012 2.08984C9.87012 1.53756 9.4224 1.08984 8.87012 1.08984C8.31783 1.08984 7.87012 1.53756 7.87012 2.08984V8.08984C7.87012 8.64213 8.31783 9.08984 8.87012 9.08984C9.4224 9.08984 9.87012 8.64213 9.87012 8.08984V2.08984Z" fill="#60BB46"/>
|
||||
<path d="M10.8702 2.50988V2.64988C10.8702 3.01988 11.0702 3.36988 11.4102 3.51988C13.6802 4.51988 15.2202 6.88988 14.9702 9.56988C14.7002 12.5599 12.1002 14.9599 9.09021 15.0099C5.74021 15.0599 2.99021 12.3499 2.99021 9.00988C2.99021 6.65988 4.35021 4.62988 6.31021 3.64988C6.64021 3.48988 6.86021 3.16988 6.86021 2.80988V2.63988C6.86021 1.94988 6.14021 1.51988 5.51021 1.81988C2.42021 3.30988 0.430214 6.71988 1.12021 10.5199C1.69021 13.6799 4.22021 16.2499 7.37021 16.8699C12.4902 17.8699 16.9802 13.9699 16.9802 9.02988C16.9802 5.71988 14.9702 2.88988 12.1002 1.66988C11.5102 1.41988 10.8502 1.88988 10.8502 2.52988L10.8702 2.50988Z" fill="#60BB46"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1008 B |
@@ -101,7 +101,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder={t(I18nKey.FEEDBACK$EMAIL_PLACEHOLDER)}
|
||||
className="bg-[#27272A] px-3 py-[10px] rounded-sm"
|
||||
className="bg-[#27272A] px-3 py-[10px] rounded"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export function BranchErrorState() {
|
||||
return (
|
||||
<div
|
||||
data-testid="branch-dropdown-error"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm text-red-500"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
|
||||
>
|
||||
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_BRANCHES")}</span>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function BranchLoadingState() {
|
||||
return (
|
||||
<div
|
||||
data-testid="branch-dropdown-loading"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">{t("HOME$LOADING_BRANCHES")}</span>
|
||||
|
||||
@@ -6,7 +6,7 @@ export function RepositoryErrorState() {
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-error"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm text-red-500"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
|
||||
>
|
||||
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_REPOSITORIES")}</span>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function RepositoryLoadingState() {
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-loading"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">{t("HOME$LOADING_REPOSITORIES")}</span>
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Thumbnail({ src, size = "small" }: ThumbnailProps) {
|
||||
alt=""
|
||||
src={src}
|
||||
className={cn(
|
||||
"rounded-sm object-cover",
|
||||
"rounded object-cover",
|
||||
size === "small" && "w-[62px] h-[62px]",
|
||||
size === "large" && "w-[100px] h-[100px]",
|
||||
)}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function PaymentForm() {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded-sm px-3 py-2",
|
||||
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded px-3 py-2",
|
||||
"text-[28px] leading-8 -tracking-[0.02em] font-bold",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function BrandButton({
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-fit p-2 text-sm rounded-sm disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
|
||||
"w-fit p-2 text-sm rounded disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
|
||||
variant === "primary" && "bg-primary text-[#0D0F11]",
|
||||
variant === "secondary" && "border border-primary text-primary",
|
||||
variant === "danger" && "bg-red-600 text-white hover:bg-red-700",
|
||||
|
||||
@@ -69,7 +69,7 @@ export function MCPJsonEditor({ mcpConfig, onChange }: MCPJsonEditorProps) {
|
||||
{t(I18nKey.SETTINGS$MCP_CONFIG_DESCRIPTION)}
|
||||
</div>
|
||||
<textarea
|
||||
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-hidden"
|
||||
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-none"
|
||||
value={configText}
|
||||
onChange={handleTextChange}
|
||||
spellCheck="false"
|
||||
|
||||
@@ -158,7 +158,7 @@ export function SecretForm({
|
||||
required
|
||||
className={cn(
|
||||
"resize-none",
|
||||
"bg-tertiary border border-[#717888] rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
|
||||
)}
|
||||
rows={8}
|
||||
@@ -177,7 +177,7 @@ export function SecretForm({
|
||||
defaultValue={secretDescription}
|
||||
className={cn(
|
||||
"resize-none",
|
||||
"bg-tertiary border border-[#717888] rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -63,7 +63,7 @@ export function SettingsDropdownInput({
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
defaultFilter={defaultFilter}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function SettingsInput({
|
||||
required={required}
|
||||
pattern={pattern}
|
||||
className={cn(
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"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",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function ReplaySuggestionBox({ onChange }: ReplaySuggestionBoxProps) {
|
||||
htmlFor="import-trajectory"
|
||||
className="w-full flex justify-center"
|
||||
>
|
||||
<span className="border-2 border-dashed border-neutral-600 rounded-sm px-2 py-1 cursor-pointer">
|
||||
<span className="border-2 border-dashed border-neutral-600 rounded px-2 py-1 cursor-pointer">
|
||||
{t(I18nKey.LANDING$UPLOAD_TRAJECTORY)}
|
||||
</span>
|
||||
<input
|
||||
|
||||
@@ -18,7 +18,7 @@ export function EditorActionButton({
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"text-sm py-0.5 rounded-sm w-20",
|
||||
"text-sm py-0.5 rounded w-20",
|
||||
"hover:bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function ModalButton({
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
variant === "default" && "text-sm font-[500] py-[10px] rounded-sm",
|
||||
variant === "default" && "text-sm font-[500] py-[10px] rounded",
|
||||
variant === "text-like" && "text-xs leading-4 font-normal",
|
||||
icon && "flex items-center justify-center gap-2",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
|
||||
@@ -36,7 +36,7 @@ export function CustomInput({
|
||||
required={required}
|
||||
defaultValue={defaultValue}
|
||||
type={type}
|
||||
className="bg-[#27272A] text-xs py-[10px] px-3 rounded-sm"
|
||||
className="bg-[#27272A] text-xs py-[10px] px-3 rounded"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ export function ErrorToast({ id, error }: ErrorToastProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toast.dismiss(id)}
|
||||
className="bg-neutral-500 px-1 rounded-sm h-full"
|
||||
className="bg-neutral-500 px-1 rounded h-full"
|
||||
>
|
||||
{t(I18nKey.ERROR_TOAST$CLOSE_BUTTON_LABEL)}
|
||||
</button>
|
||||
|
||||
@@ -269,19 +269,19 @@ function SecurityInvariant() {
|
||||
<hr className="border-t border-neutral-600 my-2" />
|
||||
<ul className="space-y-2">
|
||||
<div
|
||||
className={`cursor-pointer p-2 rounded-sm ${activeSection === "logs" && "bg-neutral-600"}`}
|
||||
className={`cursor-pointer p-2 rounded ${activeSection === "logs" && "bg-neutral-600"}`}
|
||||
onClick={() => setActiveSection("logs")}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$LOG_LABEL)}
|
||||
</div>
|
||||
<div
|
||||
className={`cursor-pointer p-2 rounded-sm ${activeSection === "policy" && "bg-neutral-600"}`}
|
||||
className={`cursor-pointer p-2 rounded ${activeSection === "policy" && "bg-neutral-600"}`}
|
||||
onClick={() => setActiveSection("policy")}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$POLICY_LABEL)}
|
||||
</div>
|
||||
<div
|
||||
className={`cursor-pointer p-2 rounded-sm ${activeSection === "settings" && "bg-neutral-600"}`}
|
||||
className={`cursor-pointer p-2 rounded ${activeSection === "settings" && "bg-neutral-600"}`}
|
||||
onClick={() => setActiveSection("settings")}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$SETTINGS_LABEL)}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function ModelSelector({
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -142,7 +142,7 @@ export function ModelSelector({
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -28,8 +28,7 @@ import {
|
||||
} from "#/types/core/guards";
|
||||
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
|
||||
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
|
||||
|
||||
export type WebSocketStatus = "CONNECTING" | "CONNECTED" | "DISCONNECTED";
|
||||
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
|
||||
|
||||
const hasValidMessageProperty = (obj: unknown): obj is { message: string } =>
|
||||
typeof obj === "object" &&
|
||||
@@ -70,7 +69,7 @@ const isMessageAction = (
|
||||
isUserMessage(event) || isAssistantMessage(event);
|
||||
|
||||
interface UseWsClient {
|
||||
webSocketStatus: WebSocketStatus;
|
||||
status: ProjectStatus;
|
||||
isLoadingMessages: boolean;
|
||||
events: Record<string, unknown>[];
|
||||
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
|
||||
@@ -78,7 +77,7 @@ interface UseWsClient {
|
||||
}
|
||||
|
||||
const WsClientContext = React.createContext<UseWsClient>({
|
||||
webSocketStatus: "DISCONNECTED",
|
||||
status: "DISCONNECTED",
|
||||
isLoadingMessages: true,
|
||||
events: [],
|
||||
parsedEvents: [],
|
||||
@@ -135,8 +134,7 @@ export function WsClientProvider({
|
||||
const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
|
||||
const queryClient = useQueryClient();
|
||||
const sioRef = React.useRef<Socket | null>(null);
|
||||
const [webSocketStatus, setWebSocketStatus] =
|
||||
React.useState<WebSocketStatus>("DISCONNECTED");
|
||||
const [status, setStatus] = React.useState<ProjectStatus>("CONNECTING");
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
const [parsedEvents, setParsedEvents] = React.useState<
|
||||
(OpenHandsAction | OpenHandsObservation)[]
|
||||
@@ -157,7 +155,7 @@ export function WsClientProvider({
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
setWebSocketStatus("CONNECTED");
|
||||
setStatus("CONNECTED");
|
||||
removeErrorMessage();
|
||||
}
|
||||
|
||||
@@ -256,7 +254,7 @@ export function WsClientProvider({
|
||||
}
|
||||
|
||||
function handleDisconnect(data: unknown) {
|
||||
setWebSocketStatus("DISCONNECTED");
|
||||
setStatus("DISCONNECTED");
|
||||
const sio = sioRef.current;
|
||||
if (!sio) {
|
||||
return;
|
||||
@@ -270,7 +268,7 @@ export function WsClientProvider({
|
||||
|
||||
function handleError(data: unknown) {
|
||||
// set status
|
||||
setWebSocketStatus("DISCONNECTED");
|
||||
setStatus("DISCONNECTED");
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
|
||||
setErrorMessage(
|
||||
@@ -289,14 +287,17 @@ export function WsClientProvider({
|
||||
// reset events when conversationId changes
|
||||
setEvents([]);
|
||||
setParsedEvents([]);
|
||||
setWebSocketStatus("CONNECTING");
|
||||
setStatus("CONNECTING");
|
||||
}, [conversationId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!conversationId) {
|
||||
throw new Error("No conversation ID provided");
|
||||
}
|
||||
if (conversation?.status !== "RUNNING" && !conversation?.runtime_status) {
|
||||
if (
|
||||
!conversation ||
|
||||
["STOPPED", "STARTING"].includes(conversation.status)
|
||||
) {
|
||||
return () => undefined; // conversation not yet loaded
|
||||
}
|
||||
|
||||
@@ -306,9 +307,6 @@ export function WsClientProvider({
|
||||
sio.disconnect();
|
||||
}
|
||||
|
||||
// Set initial status...
|
||||
setWebSocketStatus("CONNECTING");
|
||||
|
||||
const lastEvent = lastEventRef.current;
|
||||
const query = {
|
||||
latest_event_id: lastEvent?.id ?? -1,
|
||||
@@ -343,12 +341,7 @@ export function WsClientProvider({
|
||||
sio.off("connect_failed", handleError);
|
||||
sio.off("disconnect", handleDisconnect);
|
||||
};
|
||||
}, [
|
||||
conversationId,
|
||||
conversation?.url,
|
||||
conversation?.status,
|
||||
conversation?.runtime_status,
|
||||
]);
|
||||
}, [conversationId, conversation?.url, conversation?.status]);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
@@ -363,18 +356,13 @@ export function WsClientProvider({
|
||||
|
||||
const value = React.useMemo<UseWsClient>(
|
||||
() => ({
|
||||
webSocketStatus,
|
||||
status,
|
||||
isLoadingMessages: messageRateHandler.isUnderThreshold,
|
||||
events,
|
||||
parsedEvents,
|
||||
send,
|
||||
}),
|
||||
[
|
||||
webSocketStatus,
|
||||
messageRateHandler.isUnderThreshold,
|
||||
events,
|
||||
parsedEvents,
|
||||
],
|
||||
[status, messageRateHandler.isUnderThreshold, events, parsedEvents],
|
||||
);
|
||||
|
||||
return <WsClientContext value={value}>{children}</WsClientContext>;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useRuntimeIsReady } from "../use-runtime-is-ready";
|
||||
|
||||
export const useConversationConfig = () => {
|
||||
const { status } = useWsClient();
|
||||
const { conversationId } = useConversationId();
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["conversation_config", conversationId],
|
||||
@@ -14,7 +14,7 @@ export const useConversationConfig = () => {
|
||||
if (!conversationId) throw new Error("No conversation ID");
|
||||
return OpenHands.getRuntimeId(conversationId);
|
||||
},
|
||||
enabled: runtimeIsReady && !!conversationId,
|
||||
enabled: status !== "DISCONNECTED" && !!conversationId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
interface UseConversationMicroagentsOptions {
|
||||
conversationId: string | undefined;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const useConversationMicroagents = ({
|
||||
conversationId,
|
||||
enabled = true,
|
||||
}: UseConversationMicroagentsOptions) =>
|
||||
useQuery({
|
||||
queryKey: ["conversation", conversationId, "microagents"],
|
||||
queryFn: async () => {
|
||||
if (!conversationId) {
|
||||
throw new Error("No conversation ID provided");
|
||||
}
|
||||
const data = await OpenHands.getMicroagents(conversationId);
|
||||
return data.microagents;
|
||||
},
|
||||
enabled: !!conversationId && enabled,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useConversationId } from "../use-conversation-id";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useGetMicroagentPrompt = ({ eventId }: { eventId: number }) => {
|
||||
const { conversationId } = useConversationId();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["conversation", "remember_prompt", conversationId, eventId],
|
||||
queryFn: () => OpenHands.getMicroagentPrompt(conversationId, eventId),
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,15 @@
|
||||
// this file generate by script, don't modify it manually!!!
|
||||
export enum I18nKey {
|
||||
MICROAGENT$NO_REPOSITORY_FOUND = "MICROAGENT$NO_REPOSITORY_FOUND",
|
||||
MICROAGENT$ADD_TO_MICROAGENT = "MICROAGENT$ADD_TO_MICROAGENT",
|
||||
MICROAGENT$WHAT_TO_ADD = "MICROAGENT$WHAT_TO_ADD",
|
||||
MICROAGENT$WHERE_TO_PUT = "MICROAGENT$WHERE_TO_PUT",
|
||||
MICROAGENT$ADD_TRIGGER = "MICROAGENT$ADD_TRIGGER",
|
||||
MICROAGENT$WAIT_FOR_RUNTIME = "MICROAGENT$WAIT_FOR_RUNTIME",
|
||||
MICROAGENT$ADDING_CONTEXT = "MICROAGENT$ADDING_CONTEXT",
|
||||
MICROAGENT$VIEW_CONVERSATION = "MICROAGENT$VIEW_CONVERSATION",
|
||||
MICROAGENT$SUCCESS_PR_READY = "MICROAGENT$SUCCESS_PR_READY",
|
||||
STATUS$CONNECTING_TO_RUNTIME = "STATUS$CONNECTING_TO_RUNTIME",
|
||||
STATUS$WEBSOCKET_CLOSED = "STATUS$WEBSOCKET_CLOSED",
|
||||
HOME$LAUNCH_FROM_SCRATCH = "HOME$LAUNCH_FROM_SCRATCH",
|
||||
HOME$READ_THIS = "HOME$READ_THIS",
|
||||
@@ -248,9 +258,6 @@ export enum I18nKey {
|
||||
INVARIANT$TRACE_EXPORTED_MESSAGE = "INVARIANT$TRACE_EXPORTED_MESSAGE",
|
||||
INVARIANT$POLICY_UPDATED_MESSAGE = "INVARIANT$POLICY_UPDATED_MESSAGE",
|
||||
INVARIANT$SETTINGS_UPDATED_MESSAGE = "INVARIANT$SETTINGS_UPDATED_MESSAGE",
|
||||
CHAT_INTERFACE$DISCONNECTED = "CHAT_INTERFACE$DISCONNECTED",
|
||||
CHAT_INTERFACE$CONNECTING = "CHAT_INTERFACE$CONNECTING",
|
||||
CHAT_INTERFACE$STOPPED = "CHAT_INTERFACE$STOPPED",
|
||||
CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE = "CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE",
|
||||
CHAT_INTERFACE$AGENT_INIT_MESSAGE = "CHAT_INTERFACE$AGENT_INIT_MESSAGE",
|
||||
CHAT_INTERFACE$AGENT_RUNNING_MESSAGE = "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE",
|
||||
@@ -369,8 +376,10 @@ export enum I18nKey {
|
||||
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
|
||||
FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL",
|
||||
SIDEBAR$CONVERSATIONS = "SIDEBAR$CONVERSATIONS",
|
||||
STATUS$CONNECTING_TO_RUNTIME = "STATUS$CONNECTING_TO_RUNTIME",
|
||||
STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME",
|
||||
STATUS$STARTING_CONTAINER = "STATUS$STARTING_CONTAINER",
|
||||
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
|
||||
STATUS$CONTAINER_STARTED = "STATUS$CONTAINER_STARTED",
|
||||
STATUS$SETTING_UP_WORKSPACE = "STATUS$SETTING_UP_WORKSPACE",
|
||||
STATUS$SETTING_UP_GIT_HOOKS = "STATUS$SETTING_UP_GIT_HOOKS",
|
||||
ACCOUNT_SETTINGS_MODAL$DISCONNECT = "ACCOUNT_SETTINGS_MODAL$DISCONNECT",
|
||||
@@ -461,7 +470,7 @@ export enum I18nKey {
|
||||
CONVERSATION$NO_CONVERSATIONS = "CONVERSATION$NO_CONVERSATIONS",
|
||||
LANDING$SELECT_GIT_REPO = "LANDING$SELECT_GIT_REPO",
|
||||
BUTTON$SEND = "BUTTON$SEND",
|
||||
STATUS$BUILDING_RUNTIME = "STATUS$BUILDING_RUNTIME",
|
||||
STATUS$WAITING_FOR_CLIENT = "STATUS$WAITING_FOR_CLIENT",
|
||||
SUGGESTIONS$WHAT_TO_BUILD = "SUGGESTIONS$WHAT_TO_BUILD",
|
||||
SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL = "SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL",
|
||||
BUTTON$MARK_HELPFUL = "BUTTON$MARK_HELPFUL",
|
||||
@@ -545,16 +554,6 @@ export enum I18nKey {
|
||||
TOS$CONTINUE = "TOS$CONTINUE",
|
||||
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
|
||||
TIPS$CUSTOMIZE_MICROAGENT = "TIPS$CUSTOMIZE_MICROAGENT",
|
||||
CONVERSATION$SHOW_MICROAGENTS = "CONVERSATION$SHOW_MICROAGENTS",
|
||||
CONVERSATION$NO_MICROAGENTS = "CONVERSATION$NO_MICROAGENTS",
|
||||
CONVERSATION$FAILED_TO_FETCH_MICROAGENTS = "CONVERSATION$FAILED_TO_FETCH_MICROAGENTS",
|
||||
MICROAGENTS_MODAL$TITLE = "MICROAGENTS_MODAL$TITLE",
|
||||
MICROAGENTS_MODAL$TRIGGERS = "MICROAGENTS_MODAL$TRIGGERS",
|
||||
MICROAGENTS_MODAL$INPUTS = "MICROAGENTS_MODAL$INPUTS",
|
||||
MICROAGENTS_MODAL$TOOLS = "MICROAGENTS_MODAL$TOOLS",
|
||||
MICROAGENTS_MODAL$CONTENT = "MICROAGENTS_MODAL$CONTENT",
|
||||
MICROAGENTS_MODAL$NO_CONTENT = "MICROAGENTS_MODAL$NO_CONTENT",
|
||||
MICROAGENTS_MODAL$FETCH_ERROR = "MICROAGENTS_MODAL$FETCH_ERROR",
|
||||
TIPS$SETUP_SCRIPT = "TIPS$SETUP_SCRIPT",
|
||||
TIPS$VSCODE_INSTANCE = "TIPS$VSCODE_INSTANCE",
|
||||
TIPS$SAVE_WORK = "TIPS$SAVE_WORK",
|
||||
|
||||
@@ -3967,69 +3967,21 @@
|
||||
"ja": "設定を更新しました",
|
||||
"uk": "Налаштування оновлено"
|
||||
},
|
||||
"CHAT_INTERFACE$DISCONNECTED": {
|
||||
"en": "Disconnected",
|
||||
"ja": "切断されました",
|
||||
"zh-CN": "已断开连接",
|
||||
"zh-TW": "已斷開連接",
|
||||
"ko-KR": "연결 끊김",
|
||||
"no": "Frakoblet",
|
||||
"it": "Disconnesso",
|
||||
"pt": "Desconectado",
|
||||
"es": "Desconectado",
|
||||
"ar": "تم قطع الاتصال",
|
||||
"fr": "Déconnecté",
|
||||
"tr": "Bağlantı kesildi",
|
||||
"de": "Getrennt",
|
||||
"uk": "Від'єднано"
|
||||
},
|
||||
"CHAT_INTERFACE$CONNECTING": {
|
||||
"en": "Connecting",
|
||||
"ja": "接続中",
|
||||
"zh-CN": "正在连接",
|
||||
"zh-TW": "正在連接",
|
||||
"ko-KR": "연결 중",
|
||||
"no": "Kobler til",
|
||||
"it": "Connessione in corso",
|
||||
"pt": "Conectando",
|
||||
"es": "Conectando",
|
||||
"ar": "جاري الاتصال",
|
||||
"fr": "Connexion en cours",
|
||||
"tr": "Bağlanıyor",
|
||||
"de": "Verbindung wird hergestellt",
|
||||
"uk": "З'єднання"
|
||||
},
|
||||
"CHAT_INTERFACE$STOPPED": {
|
||||
"en": "Stopped",
|
||||
"ja": "停止しました",
|
||||
"zh-CN": "已停止",
|
||||
"zh-TW": "已停止",
|
||||
"ko-KR": "중지됨",
|
||||
"no": "Stoppet",
|
||||
"it": "Fermato",
|
||||
"pt": "Parado",
|
||||
"es": "Detenido",
|
||||
"ar": "توقف",
|
||||
"fr": "Arrêté",
|
||||
"tr": "Durduruldu",
|
||||
"de": "Angehalten",
|
||||
"uk": "Зупинено"
|
||||
},
|
||||
"CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": {
|
||||
"en": "Initializing Agent...",
|
||||
"de": "Agent wird initialisiert...",
|
||||
"zh-CN": "正在初始化智能体...",
|
||||
"zh-TW": "正在初始化智能體...",
|
||||
"ko-KR": "에이전트 초기화 중...",
|
||||
"no": "Initialiserer agent...",
|
||||
"it": "Inizializzazione dell'agente...",
|
||||
"pt": "Inicializando o Agente...",
|
||||
"es": "Inicializando Agente...",
|
||||
"ar": "جارٍ تهيئة الوكيل...",
|
||||
"fr": "Initialisation de l'agent...",
|
||||
"tr": "Ajan başlatılıyor...",
|
||||
"ja": "エージェントを初期化中...",
|
||||
"uk": "Ініціалізація агента..."
|
||||
"en": "Starting up!",
|
||||
"de": "Wird gestartet!",
|
||||
"zh-CN": "正在启动!",
|
||||
"zh-TW": "正在啟動!",
|
||||
"ko-KR": "시작 중입니다!",
|
||||
"no": "Starter opp!",
|
||||
"it": "Avvio in corso!",
|
||||
"pt": "Iniciando!",
|
||||
"es": "¡Iniciando!",
|
||||
"ar": "جارٍ البدء!",
|
||||
"fr": "Démarrage en cours !",
|
||||
"tr": "Başlatılıyor!",
|
||||
"ja": "起動中!",
|
||||
"uk": "Запуск!"
|
||||
},
|
||||
"CHAT_INTERFACE$AGENT_INIT_MESSAGE": {
|
||||
"en": "Agent is initialized, waiting for task...",
|
||||
@@ -5935,6 +5887,54 @@
|
||||
"ja": "ランタイムを開始中",
|
||||
"uk": "Запуск середовища виконання..."
|
||||
},
|
||||
"STATUS$STARTING_CONTAINER": {
|
||||
"en": "Preparing container, this might take a few minutes...",
|
||||
"zh-CN": "正在准备容器,这可能需要几分钟...",
|
||||
"zh-TW": "正在準備容器,這可能需要幾分鐘...",
|
||||
"de": "Container wird vorbereitet, dies kann einige Minuten dauern...",
|
||||
"ko-KR": "컨테이너를 준비 중입니다. 몇 분 정도 걸릴 수 있습니다...",
|
||||
"no": "Forbereder container, dette kan ta noen minutter...",
|
||||
"it": "Preparazione del container in corso, potrebbe richiedere alcuni minuti...",
|
||||
"pt": "Preparando o container, isso pode levar alguns minutos...",
|
||||
"es": "Preparando el contenedor, esto puede tardar unos minutos...",
|
||||
"ar": "جارٍ إعداد الحاوية، قد يستغرق هذا بضع دقائق...",
|
||||
"fr": "Préparation du conteneur, cela peut prendre quelques minutes...",
|
||||
"tr": "Konteyner hazırlanıyor, bu işlem birkaç dakika sürebilir...",
|
||||
"ja": "コンテナを開始中",
|
||||
"uk": "Підготовка контейнера, це може тривати кілька хвилин..."
|
||||
},
|
||||
"STATUS$PREPARING_CONTAINER": {
|
||||
"en": "Preparing to start container...",
|
||||
"zh-CN": "正在准备启动容器...",
|
||||
"zh-TW": "正在準備啟動容器...",
|
||||
"de": "Vorbereitung zum Starten des Containers...",
|
||||
"ko-KR": "컨테이너 시작 준비 중...",
|
||||
"no": "Forbereder å starte container...",
|
||||
"it": "Preparazione all'avvio del container...",
|
||||
"pt": "Preparando para iniciar o container...",
|
||||
"es": "Preparando para iniciar el contenedor...",
|
||||
"ar": "جارٍ التحضير لبدء الحاوية...",
|
||||
"fr": "Préparation du démarrage du conteneur...",
|
||||
"tr": "Konteyner başlatılmaya hazırlanıyor...",
|
||||
"ja": "コンテナを準備中",
|
||||
"uk": "Preparing to start container..."
|
||||
},
|
||||
"STATUS$CONTAINER_STARTED": {
|
||||
"en": "Container started.",
|
||||
"zh-CN": "容器已启动。",
|
||||
"zh-TW": "容器已啟動。",
|
||||
"de": "Container gestartet.",
|
||||
"ko-KR": "컨테이너가 시작되었습니다.",
|
||||
"no": "Container startet.",
|
||||
"it": "Container avviato.",
|
||||
"pt": "Container iniciado.",
|
||||
"es": "Contenedor iniciado.",
|
||||
"ar": "تم بدء الحاوية.",
|
||||
"fr": "Conteneur démarré.",
|
||||
"tr": "Konteyner başlatıldı.",
|
||||
"ja": "コンテナが開始されました",
|
||||
"uk": "Контейнер запущено."
|
||||
},
|
||||
"STATUS$SETTING_UP_WORKSPACE": {
|
||||
"en": "Setting up workspace...",
|
||||
"zh-CN": "正在设置工作区...",
|
||||
@@ -7375,21 +7375,21 @@
|
||||
"tr": "Gönder",
|
||||
"uk": "Надіслати"
|
||||
},
|
||||
"STATUS$BUILDING_RUNTIME": {
|
||||
"en": "Building Runtime...",
|
||||
"ja": "ランタイムを構築中...",
|
||||
"zh-CN": "正在构建运行时...",
|
||||
"zh-TW": "正在構建運行時...",
|
||||
"ko-KR": "런타임 구축 중...",
|
||||
"no": "Bygger kjøretidsmiljø...",
|
||||
"it": "Costruzione del runtime in corso...",
|
||||
"pt": "Construindo ambiente de execução...",
|
||||
"es": "Construyendo entorno de ejecución...",
|
||||
"ar": "جاري بناء بيئة التشغيل...",
|
||||
"fr": "Construction de l'environnement d'exécution...",
|
||||
"tr": "Çalışma ortamı oluşturuluyor...",
|
||||
"de": "Laufzeitumgebung wird erstellt...",
|
||||
"uk": "Створення середовища виконання..."
|
||||
"STATUS$WAITING_FOR_CLIENT": {
|
||||
"en": "Waiting for client to become ready...",
|
||||
"zh-CN": "等待客户端准备就绪...",
|
||||
"zh-TW": "等待客戶端準備就緒...",
|
||||
"de": "Warten auf Bereitschaft des Clients...",
|
||||
"ko-KR": "클라이언트가 준비될 때까지 기다리는 중...",
|
||||
"no": "Venter på at klienten skal bli klar...",
|
||||
"it": "In attesa che il client sia pronto...",
|
||||
"pt": "Aguardando o cliente ficar pronto...",
|
||||
"es": "Esperando a que el cliente esté listo...",
|
||||
"ar": "في انتظار جاهزية العميل...",
|
||||
"fr": "En attente que le client soit prêt...",
|
||||
"tr": "İstemcinin hazır olması bekleniyor...",
|
||||
"ja": "クライアントの準備を待機中",
|
||||
"uk": "Чекаємо на готовність клієнта..."
|
||||
},
|
||||
"SUGGESTIONS$WHAT_TO_BUILD": {
|
||||
"en": "What do you want to build?",
|
||||
@@ -8704,180 +8704,20 @@
|
||||
"tr": "Hizmet Şartlarını kabul ederken hata oluştu"
|
||||
},
|
||||
"TIPS$CUSTOMIZE_MICROAGENT": {
|
||||
"en": "You can customize OpenHands for your repo using an available microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",
|
||||
"ja": "利用可能なマイクロエージェントを使用して、リポジトリ用にOpenHandsをカスタマイズできます。OpenHandsに、コードの実行方法を含むリポジトリの説明を.openhands/microagents/repo.mdに入れるよう依頼してください。",
|
||||
"zh-CN": "您可以使用可用微代理为您的仓库自定义OpenHands。请OpenHands将仓库的描述(包括如何运行代码)放入.openhands/microagents/repo.md。",
|
||||
"zh-TW": "您可以使用可用微代理為您的倉庫自定義OpenHands。請OpenHands將倉庫的描述(包括如何運行代碼)放入.openhands/microagents/repo.md。",
|
||||
"ko-KR": "사용 가능한 마이크로에이전트를 사용하여 저장소에 맞게 OpenHands를 사용자 정의할 수 있습니다. OpenHands에게 코드 실행 방법을 포함한 저장소 설명을 .openhands/microagents/repo.md에 넣도록 요청하세요.",
|
||||
"no": "Du kan tilpasse OpenHands for ditt repo ved å bruke en tilgjengelig mikroagent. Be OpenHands om å legge en beskrivelse av repoet, inkludert hvordan du kjører koden, i .openhands/microagents/repo.md.",
|
||||
"it": "Puoi personalizzare OpenHands per il tuo repository utilizzando un microagente disponibile. Chiedi a OpenHands di inserire una descrizione del repository, incluso come eseguire il codice, in .openhands/microagents/repo.md.",
|
||||
"pt": "Você pode personalizar o OpenHands para seu repositório usando um microagente disponível. Peça ao OpenHands para colocar uma descrição do repositório, incluindo como executar o código, em .openhands/microagents/repo.md.",
|
||||
"es": "Puede personalizar OpenHands para su repositorio utilizando un microagente disponible. Pídale a OpenHands que ponga una descripción del repositorio, incluido cómo ejecutar el código, en .openhands/microagents/repo.md.",
|
||||
"ar": "يمكنك تخصيص OpenHands لمستودعك باستخدام وكيل مصغر متاح. اطلب من OpenHands وضع وصف للمستودع، بما في ذلك كيفية تشغيل الكود، في .openhands/microagents/repo.md.",
|
||||
"de": "Sie können OpenHands für Ihr Repository mit einem verfügbaren Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren.",
|
||||
"fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent disponible. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.",
|
||||
"tr": "Kullanılabilir bir mikro ajan kullanarak OpenHands'i deponuz için özelleştirebilirsiniz. OpenHands'ten deponun açıklamasını, kodun nasıl çalıştırılacağı dahil, .openhands/microagents/repo.md dosyasına koymasını isteyin.",
|
||||
"uk": "Ви можете налаштувати OpenHands для свого репозиторію за допомогою доступного мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інформацію про те, як запустити код, у файлі .openhands/microagents/repo.md."
|
||||
},
|
||||
"CONVERSATION$SHOW_MICROAGENTS": {
|
||||
"en": "Show Available Microagents",
|
||||
"ja": "利用可能なマイクロエージェントを表示",
|
||||
"zh-CN": "显示可用微代理",
|
||||
"zh-TW": "顯示可用微代理",
|
||||
"ko-KR": "사용 가능한 마이크로에이전트 표시",
|
||||
"no": "Vis tilgjengelige mikroagenter",
|
||||
"ar": "عرض الوكلاء المصغرين المتاحة",
|
||||
"de": "Verfügbare Mikroagenten anzeigen",
|
||||
"fr": "Afficher les micro-agents disponibles",
|
||||
"it": "Mostra microagenti disponibili",
|
||||
"pt": "Mostrar microagentes disponíveis",
|
||||
"es": "Mostrar microagentes disponibles",
|
||||
"tr": "Kullanılabilir mikro ajanları göster",
|
||||
"uk": "Показати доступних мікроагентів"
|
||||
},
|
||||
"CONVERSATION$NO_MICROAGENTS": {
|
||||
"en": "No available microagents found for this conversation.",
|
||||
"ja": "この会話用の利用可能なマイクロエージェントが見つかりませんでした。",
|
||||
"zh-CN": "未找到此对话的可用微代理。",
|
||||
"zh-TW": "未找到此對話的可用微代理。",
|
||||
"ko-KR": "이 대화에 대한 사용 가능한 마이크로에이전트를 찾을 수 없습니다.",
|
||||
"no": "Ingen tilgjengelige mikroagenter funnet for denne samtalen.",
|
||||
"ar": "لم يتم العثور على وكلاء مصغرين متاحة لهذه المحادثة.",
|
||||
"de": "Keine verfügbaren Mikroagenten für dieses Gespräch gefunden.",
|
||||
"fr": "Aucun micro-agent disponible trouvé pour cette conversation.",
|
||||
"it": "Nessun microagente disponibile trovato per questa conversazione.",
|
||||
"pt": "Nenhum microagente disponível encontrado para esta conversa.",
|
||||
"es": "No se encontraron microagentes disponibles para esta conversación.",
|
||||
"tr": "Bu konuşma için kullanılabilir mikro ajan bulunamadı.",
|
||||
"uk": "Для цієї розмови не знайдено доступних мікроагентів."
|
||||
},
|
||||
"CONVERSATION$FAILED_TO_FETCH_MICROAGENTS": {
|
||||
"en": "Failed to fetch available microagents",
|
||||
"ja": "利用可能なマイクロエージェントの取得に失敗しました",
|
||||
"zh-CN": "获取可用微代理失败",
|
||||
"zh-TW": "獲取可用微代理失敗",
|
||||
"ko-KR": "사용 가능한 마이크로에이전트를 가져오지 못했습니다",
|
||||
"no": "Kunne ikke hente tilgjengelige mikroagenter",
|
||||
"ar": "فشل في جلب الوكلاء المصغرين المتاحة",
|
||||
"de": "Fehler beim Abrufen von verfügbaren Mikroagenten",
|
||||
"fr": "Échec de la récupération des micro-agents disponibles",
|
||||
"it": "Impossibile recuperare i microagenti disponibili",
|
||||
"pt": "Falha ao buscar microagentes disponíveis",
|
||||
"es": "Error al obtener microagentes disponibles",
|
||||
"tr": "Kullanılabilir mikro ajanlar getirilemedi",
|
||||
"uk": "Не вдалося отримати доступних мікроагентів"
|
||||
},
|
||||
"MICROAGENTS_MODAL$TITLE": {
|
||||
"en": "Available Microagents",
|
||||
"ja": "利用可能なマイクロエージェント",
|
||||
"zh-CN": "可用微代理",
|
||||
"zh-TW": "可用微代理",
|
||||
"ko-KR": "사용 가능한 마이크로에이전트",
|
||||
"no": "Tilgjengelige mikroagenter",
|
||||
"ar": "الوكلاء المصغرين المتاحة",
|
||||
"de": "Verfügbare Mikroagenten",
|
||||
"fr": "Micro-agents disponibles",
|
||||
"it": "Microagenti disponibili",
|
||||
"pt": "Microagentes disponíveis",
|
||||
"es": "Microagentes disponibles",
|
||||
"tr": "Kullanılabilir mikro ajanlar",
|
||||
"uk": "Доступні мікроагенти"
|
||||
},
|
||||
"MICROAGENTS_MODAL$TRIGGERS": {
|
||||
"en": "Triggers",
|
||||
"ja": "トリガー",
|
||||
"zh-CN": "触发器",
|
||||
"zh-TW": "觸發器",
|
||||
"ko-KR": "트리거",
|
||||
"no": "Utløsere",
|
||||
"ar": "المحفزات",
|
||||
"de": "Auslöser",
|
||||
"fr": "Déclencheurs",
|
||||
"it": "Trigger",
|
||||
"pt": "Gatilhos",
|
||||
"es": "Disparadores",
|
||||
"tr": "Tetikleyiciler",
|
||||
"uk": "Тригери"
|
||||
},
|
||||
"MICROAGENTS_MODAL$INPUTS": {
|
||||
"en": "Inputs",
|
||||
"ja": "入力",
|
||||
"zh-CN": "输入",
|
||||
"zh-TW": "輸入",
|
||||
"ko-KR": "입력",
|
||||
"no": "Inndata",
|
||||
"ar": "المدخلات",
|
||||
"de": "Eingaben",
|
||||
"fr": "Entrées",
|
||||
"it": "Input",
|
||||
"pt": "Entradas",
|
||||
"es": "Entradas",
|
||||
"tr": "Girdiler",
|
||||
"uk": "Вхідні дані"
|
||||
},
|
||||
"MICROAGENTS_MODAL$TOOLS": {
|
||||
"en": "Tools",
|
||||
"ja": "ツール",
|
||||
"zh-CN": "工具",
|
||||
"zh-TW": "工具",
|
||||
"ko-KR": "도구",
|
||||
"no": "Verktøy",
|
||||
"ar": "الأدوات",
|
||||
"de": "Werkzeuge",
|
||||
"fr": "Outils",
|
||||
"it": "Strumenti",
|
||||
"pt": "Ferramentas",
|
||||
"es": "Herramientas",
|
||||
"tr": "Araçlar",
|
||||
"uk": "Інструменти"
|
||||
},
|
||||
"MICROAGENTS_MODAL$CONTENT": {
|
||||
"en": "Content",
|
||||
"ja": "コンテンツ",
|
||||
"zh-CN": "内容",
|
||||
"zh-TW": "內容",
|
||||
"ko-KR": "콘텐츠",
|
||||
"no": "Innhold",
|
||||
"ar": "المحتوى",
|
||||
"de": "Inhalt",
|
||||
"fr": "Contenu",
|
||||
"it": "Contenuto",
|
||||
"pt": "Conteúdo",
|
||||
"es": "Contenido",
|
||||
"tr": "İçerik",
|
||||
"uk": "Вміст"
|
||||
},
|
||||
"MICROAGENTS_MODAL$NO_CONTENT": {
|
||||
"en": "Microagent has no content",
|
||||
"ja": "マイクロエージェントにコンテンツがありません",
|
||||
"zh-CN": "微代理没有内容",
|
||||
"zh-TW": "微代理沒有內容",
|
||||
"ko-KR": "마이크로에이전트에 콘텐츠가 없습니다",
|
||||
"no": "Mikroagenten har ikke innhold",
|
||||
"ar": "الوكيل المصغر ليس لديه محتوى",
|
||||
"de": "Mikroagent hat keinen Inhalt",
|
||||
"fr": "Le micro-agent n'a pas de contenu",
|
||||
"it": "Il microagente non ha contenuto",
|
||||
"pt": "Microagente não tem conteúdo",
|
||||
"es": "El microagente no tiene contenido",
|
||||
"tr": "Mikroajanın içeriği yok",
|
||||
"uk": "Мікроагент не має вмісту"
|
||||
},
|
||||
"MICROAGENTS_MODAL$FETCH_ERROR": {
|
||||
"en": "Failed to fetch microagents. Please try again later.",
|
||||
"ja": "マイクロエージェントの取得に失敗しました。後でもう一度お試しください。",
|
||||
"zh-CN": "获取微代理失败。请稍后再试。",
|
||||
"zh-TW": "獲取微代理失敗。請稍後再試。",
|
||||
"ko-KR": "마이크로에이전트를 가져오지 못했습니다. 나중에 다시 시도해 주세요.",
|
||||
"no": "Kunne ikke hente mikroagenter. Prøv igjen senere.",
|
||||
"ar": "فشل في جلب الوكلاء المصغرين. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"de": "Mikroagenten konnten nicht abgerufen werden. Bitte versuchen Sie es später erneut.",
|
||||
"fr": "Échec de la récupération des micro-agents. Veuillez réessayer plus tard.",
|
||||
"it": "Impossibile recuperare i microagenti. Riprova più tardi.",
|
||||
"pt": "Falha ao buscar microagentes. Por favor, tente novamente mais tarde.",
|
||||
"es": "Error al obtener microagentes. Por favor, inténtelo de nuevo más tarde.",
|
||||
"tr": "Mikroajanlar getirilemedi. Lütfen daha sonra tekrar deneyin.",
|
||||
"uk": "Не вдалося отримати мікроагентів. Будь ласка, спробуйте пізніше."
|
||||
"en": "You can customize OpenHands for your repo using a microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",
|
||||
"ja": "マイクロエージェントを使用して、リポジトリ用にOpenHandsをカスタマイズできます。OpenHandsに、コードの実行方法を含むリポジトリの説明を.openhands/microagents/repo.mdに入れるよう依頼してください。",
|
||||
"zh-CN": "您可以使用微代理为您的仓库自定义OpenHands。请OpenHands将仓库的描述(包括如何运行代码)放入.openhands/microagents/repo.md。",
|
||||
"zh-TW": "您可以使用微代理為您的倉庫自定義OpenHands。請OpenHands將倉庫的描述(包括如何運行代碼)放入.openhands/microagents/repo.md。",
|
||||
"ko-KR": "마이크로에이전트를 사용하여 저장소에 맞게 OpenHands를 사용자 정의할 수 있습니다. OpenHands에게 코드 실행 방법을 포함한 저장소 설명을 .openhands/microagents/repo.md에 넣도록 요청하세요.",
|
||||
"no": "Du kan tilpasse OpenHands for ditt repo ved å bruke en mikroagent. Be OpenHands om å legge en beskrivelse av repoet, inkludert hvordan du kjører koden, i .openhands/microagents/repo.md.",
|
||||
"it": "Puoi personalizzare OpenHands per il tuo repository utilizzando un microagente. Chiedi a OpenHands di inserire una descrizione del repository, incluso come eseguire il codice, in .openhands/microagents/repo.md.",
|
||||
"pt": "Você pode personalizar o OpenHands para seu repositório usando um microagente. Peça ao OpenHands para colocar uma descrição do repositório, incluindo como executar o código, em .openhands/microagents/repo.md.",
|
||||
"es": "Puede personalizar OpenHands para su repositorio utilizando un microagente. Pídale a OpenHands que ponga una descripción del repositorio, incluido cómo ejecutar el código, en .openhands/microagents/repo.md.",
|
||||
"ar": "يمكنك تخصيص OpenHands لمستودعك باستخدام وكيل مصغر. اطلب من OpenHands وضع وصف للمستودع، بما في ذلك كيفية تشغيل الكود، في .openhands/microagents/repo.md.",
|
||||
"fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.",
|
||||
"tr": "Bir mikro ajan kullanarak deponuz için OpenHands'i özelleştirebilirsiniz. OpenHands'ten kodun nasıl çalıştırılacağı da dahil olmak üzere deponun açıklamasını .openhands/microagents/repo.md dosyasına koymasını isteyin.",
|
||||
"de": "Sie können OpenHands für Ihr Repository mit einem Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren.",
|
||||
"uk": "Ви можете налаштувати OpenHands для вашого репозиторію за допомогою мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інструкції з запуску коду, у файлі .openhands/microagents/repo.md."
|
||||
},
|
||||
"TIPS$SETUP_SCRIPT": {
|
||||
"en": "You can add .openhands/setup.sh to your repository to automatically run a setup script every time you start an OpenHands conversation.",
|
||||
|
||||
@@ -57,7 +57,6 @@ const conversations: Conversation[] = [
|
||||
last_updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
status: "RUNNING",
|
||||
runtime_status: "STATUS$READY",
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -73,7 +72,6 @@ const conversations: Conversation[] = [
|
||||
).toISOString(),
|
||||
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: "STOPPED",
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -89,7 +87,6 @@ const conversations: Conversation[] = [
|
||||
).toISOString(),
|
||||
created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: "STOPPED",
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
@@ -285,7 +282,6 @@ export const handlers = [
|
||||
last_updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
status: "RUNNING",
|
||||
runtime_status: "STATUS$READY",
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ const chat = ws.link(`ws://${window?.location.host}/socket.io`);
|
||||
|
||||
export const handlers: WebSocketHandler[] = [
|
||||
chat.addEventListener("connection", (connection) => {
|
||||
// @ts-expect-error - MSW v2 type incompatibility
|
||||
const io = toSocketIo(connection);
|
||||
// @ts-expect-error - accessing private property for testing purposes
|
||||
const { url }: { url: URL } = io.client.connection;
|
||||
|
||||
@@ -41,11 +41,11 @@ function EmailInputSection({
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={onEmailChange}
|
||||
className={`text-base text-white p-2 bg-base-tertiary rounded-sm border ${
|
||||
className={`text-base text-white p-2 bg-base-tertiary rounded border ${
|
||||
isEmailChanged && !isEmailValid
|
||||
? "border-red-500"
|
||||
: "border-tertiary"
|
||||
} flex-grow focus:outline-hidden focus:border-transparent focus:ring-0`}
|
||||
} flex-grow focus:outline-none focus:border-transparent focus:ring-0`}
|
||||
placeholder={t("SETTINGS$USER_EMAIL_LOADING")}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
@@ -65,7 +65,7 @@ function EmailInputSection({
|
||||
type="button"
|
||||
onClick={onSaveEmail}
|
||||
disabled={!isEmailChanged || isSaving || !isEmailValid}
|
||||
className="px-4 py-2 rounded-sm bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
|
||||
className="px-4 py-2 rounded bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
|
||||
data-testid="save-email-button"
|
||||
>
|
||||
{isSaving ? t("SETTINGS$SAVING") : t("SETTINGS$SAVE")}
|
||||
@@ -76,7 +76,7 @@ function EmailInputSection({
|
||||
type="button"
|
||||
onClick={onResendVerification}
|
||||
disabled={isResendingVerification}
|
||||
className="px-4 py-2 rounded-sm bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
|
||||
className="px-4 py-2 rounded bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
|
||||
data-testid="resend-verification-button"
|
||||
>
|
||||
{isResendingVerification
|
||||
@@ -96,7 +96,7 @@ function VerificationAlert() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-sm mt-4"
|
||||
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mt-4"
|
||||
role="alert"
|
||||
>
|
||||
<p className="font-bold">{t("SETTINGS$EMAIL_VERIFICATION_REQUIRED")}</p>
|
||||
@@ -205,7 +205,7 @@ function UserSettingsScreen() {
|
||||
<div data-testid="user-settings-screen" className="flex flex-col h-full">
|
||||
<div className="p-9 flex flex-col gap-6">
|
||||
{isLoading ? (
|
||||
<div className="animate-pulse h-8 w-64 bg-tertiary rounded-sm" />
|
||||
<div className="animate-pulse h-8 w-64 bg-tertiary rounded" />
|
||||
) : (
|
||||
<EmailInputSection
|
||||
email={email}
|
||||
|
||||
@@ -76,7 +76,7 @@ function VSCodeTab() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenInNewTab}
|
||||
className="px-4 py-2 bg-primary text-white rounded-sm hover:bg-primary-dark transition-colors"
|
||||
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark transition-colors"
|
||||
>
|
||||
{t("VSCODE$OPEN_IN_NEW_TAB")}
|
||||
</button>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin '../hero.ts';
|
||||
@config "../tailwind.config.js";
|
||||
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.button-base {
|
||||
@apply bg-tertiary border border-neutral-600 rounded-xs;
|
||||
@apply bg-tertiary border border-neutral-600 rounded;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type ConversationStatus = "STARTING" | "RUNNING" | "STOPPED";
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* @deprecated This type is deprecated and will be removed in a future version.
|
||||
* Use types in `frontend/src/types/core` instead.
|
||||
*/
|
||||
export interface ActionMessage {
|
||||
id: number;
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export type RuntimeStatus =
|
||||
| "STATUS$STOPPED"
|
||||
| "STATUS$BUILDING_RUNTIME"
|
||||
| "STATUS$STARTING_RUNTIME"
|
||||
| "STATUS$RUNTIME_STARTED"
|
||||
| "STATUS$SETTING_UP_WORKSPACE"
|
||||
| "STATUS$SETTING_UP_GIT_HOOKS"
|
||||
| "STATUS$READY";
|
||||
@@ -5,10 +5,7 @@
|
||||
* @returns The URL to redirect to for OAuth
|
||||
*/
|
||||
export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
|
||||
// Use HTTPS protocol unless the host is localhost
|
||||
const protocol =
|
||||
requestUrl.hostname === "localhost" ? requestUrl.protocol : "https:";
|
||||
const redirectUri = `${protocol}//${requestUrl.host}/oauth/keycloak/callback`;
|
||||
const redirectUri = `${requestUrl.origin}/oauth/keycloak/callback`;
|
||||
let authUrl = requestUrl.hostname
|
||||
.replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev")
|
||||
.replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev")
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { WebSocketStatus } from "#/context/ws-client-provider";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { StatusMessage } from "#/types/message";
|
||||
import { RuntimeStatus } from "#/types/runtime-status";
|
||||
|
||||
export enum IndicatorColor {
|
||||
BLUE = "bg-blue-500",
|
||||
GREEN = "bg-green-500",
|
||||
ORANGE = "bg-orange-500",
|
||||
YELLOW = "bg-yellow-500",
|
||||
RED = "bg-red-500",
|
||||
DARK_ORANGE = "bg-orange-800",
|
||||
}
|
||||
|
||||
export const AGENT_STATUS_MAP: {
|
||||
[k: string]: string;
|
||||
} = {
|
||||
[AgentState.INIT]: I18nKey.CHAT_INTERFACE$AGENT_INIT_MESSAGE,
|
||||
[AgentState.RUNNING]: I18nKey.CHAT_INTERFACE$AGENT_RUNNING_MESSAGE,
|
||||
[AgentState.AWAITING_USER_INPUT]:
|
||||
I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE,
|
||||
[AgentState.PAUSED]: I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE,
|
||||
[AgentState.LOADING]:
|
||||
I18nKey.CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE,
|
||||
[AgentState.STOPPED]: I18nKey.CHAT_INTERFACE$AGENT_STOPPED_MESSAGE,
|
||||
[AgentState.FINISHED]: I18nKey.CHAT_INTERFACE$AGENT_FINISHED_MESSAGE,
|
||||
[AgentState.REJECTED]: I18nKey.CHAT_INTERFACE$AGENT_REJECTED_MESSAGE,
|
||||
[AgentState.ERROR]: I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE,
|
||||
[AgentState.AWAITING_USER_CONFIRMATION]:
|
||||
I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE,
|
||||
[AgentState.USER_CONFIRMED]:
|
||||
I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE,
|
||||
[AgentState.USER_REJECTED]:
|
||||
I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE,
|
||||
[AgentState.RATE_LIMITED]: I18nKey.CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE,
|
||||
};
|
||||
|
||||
export function getIndicatorColor(
|
||||
webSocketStatus: WebSocketStatus,
|
||||
conversationStatus: ConversationStatus | null,
|
||||
runtimeStatus: RuntimeStatus | null,
|
||||
agentState: AgentState | null,
|
||||
) {
|
||||
if (
|
||||
webSocketStatus === "DISCONNECTED" ||
|
||||
conversationStatus === "STOPPED" ||
|
||||
runtimeStatus === "STATUS$STOPPED" ||
|
||||
agentState === AgentState.STOPPED
|
||||
) {
|
||||
return IndicatorColor.RED;
|
||||
}
|
||||
// Display a yellow working icon while the runtime is starting
|
||||
if (
|
||||
conversationStatus === "STARTING" ||
|
||||
!["STATUS$READY", null].includes(runtimeStatus) ||
|
||||
(agentState != null &&
|
||||
[
|
||||
AgentState.LOADING,
|
||||
AgentState.PAUSED,
|
||||
AgentState.REJECTED,
|
||||
AgentState.RATE_LIMITED,
|
||||
].includes(agentState))
|
||||
) {
|
||||
return IndicatorColor.YELLOW;
|
||||
}
|
||||
|
||||
if (agentState === AgentState.AWAITING_USER_CONFIRMATION) {
|
||||
return IndicatorColor.ORANGE;
|
||||
}
|
||||
|
||||
if (agentState === AgentState.AWAITING_USER_INPUT) {
|
||||
return IndicatorColor.BLUE;
|
||||
}
|
||||
|
||||
// All other agent states are green
|
||||
return IndicatorColor.GREEN;
|
||||
}
|
||||
|
||||
export function getStatusCode(
|
||||
statusMessage: StatusMessage,
|
||||
webSocketStatus: WebSocketStatus,
|
||||
conversationStatus: ConversationStatus | null,
|
||||
runtimeStatus: RuntimeStatus | null,
|
||||
agentState: AgentState | null,
|
||||
) {
|
||||
if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") {
|
||||
return I18nKey.CHAT_INTERFACE$STOPPED;
|
||||
}
|
||||
if (runtimeStatus === "STATUS$BUILDING_RUNTIME") {
|
||||
return I18nKey.STATUS$BUILDING_RUNTIME;
|
||||
}
|
||||
if (runtimeStatus === "STATUS$STARTING_RUNTIME") {
|
||||
return I18nKey.STATUS$STARTING_RUNTIME;
|
||||
}
|
||||
if (webSocketStatus === "DISCONNECTED") {
|
||||
return I18nKey.CHAT_INTERFACE$DISCONNECTED;
|
||||
}
|
||||
if (webSocketStatus === "CONNECTING") {
|
||||
return I18nKey.CHAT_INTERFACE$CONNECTING;
|
||||
}
|
||||
|
||||
if (
|
||||
agentState === AgentState.LOADING &&
|
||||
statusMessage?.id &&
|
||||
statusMessage.id !== "STATUS$READY"
|
||||
) {
|
||||
return statusMessage.id;
|
||||
}
|
||||
|
||||
if (agentState) {
|
||||
return AGENT_STATUS_MAP[agentState];
|
||||
}
|
||||
|
||||
if (runtimeStatus && runtimeStatus !== "STATUS$READY" && !agentState) {
|
||||
return runtimeStatus;
|
||||
}
|
||||
|
||||
return "STATUS$ERROR"; // illegal state
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
import { heroui } from "@heroui/react";
|
||||
import typography from "@tailwindcss/typography";
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
@@ -20,5 +24,24 @@ export default {
|
||||
},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [typography],
|
||||
plugins: [
|
||||
heroui({
|
||||
defaultTheme: "dark",
|
||||
layout: {
|
||||
radius: {
|
||||
small: "5px",
|
||||
large: "20px",
|
||||
},
|
||||
},
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
primary: "#4465DB",
|
||||
logo: "#CFB755",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
typography,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import viteTsconfigPaths from "vite-tsconfig-paths";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import { configDefaults } from "vitest/config";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const {
|
||||
@@ -29,7 +28,6 @@ export default defineConfig(({ mode }) => {
|
||||
!process.env.VITEST && reactRouter(),
|
||||
viteTsconfigPaths(),
|
||||
svgr(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
port: FE_PORT,
|
||||
@@ -56,7 +54,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
ignored: ["**/node_modules/**", "**/.git/**"],
|
||||
ignored: ['**/node_modules/**', '**/.git/**'],
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
|
||||
@@ -208,7 +208,7 @@ Note:
|
||||
# for visualwebarena, webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
|
||||
# initialize and retrieve the first observation by issuing an noop OP
|
||||
# For non-benchmark browsing, the browser env starts with a blank page, and the agent is expected to first navigate to desired websites
|
||||
return BrowseInteractiveAction(browser_actions='noop(1000)', return_axtree=True)
|
||||
return BrowseInteractiveAction(browser_actions='noop(1000)')
|
||||
|
||||
for event in state.view:
|
||||
if isinstance(event, BrowseInteractiveAction):
|
||||
|
||||
@@ -158,8 +158,8 @@ async def modify_llm_settings_basic(
|
||||
provider_completer = FuzzyWordCompleter(provider_list)
|
||||
session = PromptSession(key_bindings=kb_cancel())
|
||||
|
||||
# Set default provider - prefer 'anthropic' if available, otherwise use the first provider
|
||||
provider = 'anthropic' if 'anthropic' in provider_list else provider_list[0]
|
||||
# Set default provider - use the first available provider from the list
|
||||
provider = provider_list[0] if provider_list else 'openai'
|
||||
model = None
|
||||
api_key = None
|
||||
|
||||
@@ -196,11 +196,9 @@ async def modify_llm_settings_basic(
|
||||
|
||||
# Make sure the provider exists in organized_models
|
||||
if provider not in organized_models:
|
||||
# If the provider doesn't exist, prefer 'anthropic' if available, otherwise use the first provider
|
||||
# If the provider doesn't exist, use the first available provider
|
||||
provider = (
|
||||
'anthropic'
|
||||
if 'anthropic' in organized_models
|
||||
else next(iter(organized_models.keys()))
|
||||
next(iter(organized_models.keys())) if organized_models else 'openai'
|
||||
)
|
||||
|
||||
provider_models = organized_models[provider]['models']
|
||||
@@ -215,10 +213,8 @@ async def modify_llm_settings_basic(
|
||||
]
|
||||
provider_models = VERIFIED_ANTHROPIC_MODELS + provider_models
|
||||
|
||||
# Set default model to the first model in the list (which will be a verified model if available)
|
||||
default_model = (
|
||||
provider_models[0] if provider_models else 'claude-sonnet-4-20250514'
|
||||
)
|
||||
# Set default model to the first model in the list
|
||||
default_model = provider_models[0] if provider_models else 'gpt-4'
|
||||
|
||||
# Show the default model but allow changing it
|
||||
print_formatted_text(
|
||||
|
||||
@@ -72,7 +72,6 @@ from openhands.events.observation import (
|
||||
from openhands.events.serialization.event import event_to_trajectory, truncate_content
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.metrics import Metrics, TokenUsage
|
||||
from openhands.memory.view import View
|
||||
|
||||
# note: RESUME is only available on web GUI
|
||||
TRAFFIC_CONTROL_REMINDER = (
|
||||
@@ -1162,8 +1161,7 @@ class AgentController:
|
||||
|
||||
def _handle_long_context_error(self) -> None:
|
||||
# When context window is exceeded, keep roughly half of agent interactions
|
||||
current_view = View.from_events(self.state.history)
|
||||
kept_events = self._apply_conversation_window(current_view.events)
|
||||
kept_events = self._apply_conversation_window()
|
||||
kept_event_ids = {e.id for e in kept_events}
|
||||
|
||||
self.log(
|
||||
@@ -1200,7 +1198,7 @@ class AgentController:
|
||||
EventSource.AGENT,
|
||||
)
|
||||
|
||||
def _apply_conversation_window(self, history: list[Event]) -> list[Event]:
|
||||
def _apply_conversation_window(self) -> list[Event]:
|
||||
"""Cuts history roughly in half when context window is exceeded.
|
||||
|
||||
It preserves action-observation pairs and ensures that the system message,
|
||||
@@ -1219,9 +1217,11 @@ class AgentController:
|
||||
Returns:
|
||||
Filtered list of events keeping newest half while preserving pairs and essential initial events.
|
||||
"""
|
||||
# Handle empty history
|
||||
if not history:
|
||||
if not self.state.history:
|
||||
return []
|
||||
|
||||
history = self.state.history
|
||||
|
||||
# 1. Identify essential initial events
|
||||
system_message: SystemMessageAction | None = None
|
||||
first_user_msg: MessageAction | None = None
|
||||
@@ -1238,59 +1238,50 @@ class AgentController:
|
||||
and system_message.id == history[0].id
|
||||
)
|
||||
|
||||
# Find First User Message in the history, which MUST exist
|
||||
first_user_msg = self._first_user_message(history)
|
||||
# Find First User Message, which MUST exist
|
||||
first_user_msg = self._first_user_message()
|
||||
if first_user_msg is None:
|
||||
# If not found in history, try the event stream
|
||||
first_user_msg = self._first_user_message()
|
||||
if first_user_msg is None:
|
||||
raise RuntimeError('No first user message found in the event stream.')
|
||||
self.log(
|
||||
'warning',
|
||||
'First user message not found in history. Using cached version from event stream.',
|
||||
)
|
||||
raise RuntimeError('No first user message found in the event stream.')
|
||||
|
||||
# Find the first user message index in the history
|
||||
first_user_msg_index = -1
|
||||
for i, event in enumerate(history):
|
||||
if isinstance(event, MessageAction) and event.source == EventSource.USER:
|
||||
first_user_msg = event
|
||||
first_user_msg_index = i
|
||||
break
|
||||
|
||||
# Find Recall Action and Observation related to the First User Message
|
||||
# Look for RecallAction after the first user message
|
||||
for i in range(first_user_msg_index + 1, len(history)):
|
||||
event = history[i]
|
||||
if (
|
||||
isinstance(event, RecallAction)
|
||||
and event.query == first_user_msg.content
|
||||
):
|
||||
# Found RecallAction, now look for its Observation
|
||||
recall_action = event
|
||||
for j in range(i + 1, len(history)):
|
||||
obs_event = history[j]
|
||||
# Check for Observation caused by this RecallAction
|
||||
if (
|
||||
isinstance(obs_event, Observation)
|
||||
and obs_event.cause == recall_action.id
|
||||
):
|
||||
recall_observation = obs_event
|
||||
break # Found the observation, stop inner loop
|
||||
break # Found the recall action (and maybe obs), stop outer loop
|
||||
if first_user_msg is not None and first_user_msg_index != -1:
|
||||
# Look for RecallAction after the first user message
|
||||
for i in range(first_user_msg_index + 1, len(history)):
|
||||
event = history[i]
|
||||
if (
|
||||
isinstance(event, RecallAction)
|
||||
and event.query == first_user_msg.content
|
||||
):
|
||||
# Found RecallAction, now look for its Observation
|
||||
recall_action = event
|
||||
for j in range(i + 1, len(history)):
|
||||
obs_event = history[j]
|
||||
# Check for Observation caused by this RecallAction
|
||||
if (
|
||||
isinstance(obs_event, Observation)
|
||||
and obs_event.cause == recall_action.id
|
||||
):
|
||||
recall_observation = obs_event
|
||||
break # Found the observation, stop inner loop
|
||||
break # Found the recall action (and maybe obs), stop outer loop
|
||||
|
||||
essential_events: list[Event] = []
|
||||
if system_message:
|
||||
essential_events.append(system_message)
|
||||
# Only include first user message if history is not empty
|
||||
if history:
|
||||
if first_user_msg:
|
||||
essential_events.append(first_user_msg)
|
||||
# Include recall action and observation if both exist
|
||||
if recall_action and recall_observation:
|
||||
essential_events.append(recall_action)
|
||||
essential_events.append(recall_observation)
|
||||
# Include recall action without observation for backward compatibility
|
||||
elif recall_action:
|
||||
essential_events.append(recall_action)
|
||||
# Also keep the RecallAction that triggered the essential RecallObservation
|
||||
if recall_action:
|
||||
essential_events.append(recall_action)
|
||||
if recall_observation:
|
||||
essential_events.append(recall_observation)
|
||||
|
||||
# 2. Determine the slice of recent events to potentially keep
|
||||
num_non_essential_events = len(history) - len(essential_events)
|
||||
@@ -1439,32 +1430,15 @@ class AgentController:
|
||||
return result
|
||||
return False
|
||||
|
||||
def _first_user_message(
|
||||
self, events: list[Event] | None = None
|
||||
) -> MessageAction | None:
|
||||
def _first_user_message(self) -> MessageAction | None:
|
||||
"""Get the first user message for this agent.
|
||||
|
||||
For regular agents, this is the first user message from the beginning (start_id=0).
|
||||
For delegate agents, this is the first user message after the delegate's start_id.
|
||||
|
||||
Args:
|
||||
events: Optional list of events to search through. If None, uses the event stream.
|
||||
|
||||
Returns:
|
||||
MessageAction | None: The first user message, or None if no user message found
|
||||
"""
|
||||
# If events list is provided, search through it
|
||||
if events is not None:
|
||||
return next(
|
||||
(
|
||||
e
|
||||
for e in events
|
||||
if isinstance(e, MessageAction) and e.source == EventSource.USER
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
# Otherwise, use the original event stream logic with caching
|
||||
# Return cached message if any
|
||||
if self._cached_first_user_message is not None:
|
||||
return self._cached_first_user_message
|
||||
|
||||
@@ -64,7 +64,7 @@ class OpenHandsConfig(BaseModel):
|
||||
extended: ExtendedConfig = Field(default_factory=lambda: ExtendedConfig({}))
|
||||
runtime: str = Field(default='docker')
|
||||
file_store: str = Field(default='local')
|
||||
file_store_path: str = Field(default='~/.openhands')
|
||||
file_store_path: str = Field(default='~/.openhands/file_store')
|
||||
file_store_web_hook_url: str | None = Field(default=None)
|
||||
file_store_web_hook_headers: dict | None = Field(default=None)
|
||||
save_trajectory_path: str | None = Field(default=None)
|
||||
|
||||
@@ -58,7 +58,6 @@ class SandboxConfig(BaseModel):
|
||||
remote_runtime_init_timeout: int = Field(default=180)
|
||||
remote_runtime_api_timeout: int = Field(default=10)
|
||||
remote_runtime_enable_retries: bool = Field(default=True)
|
||||
retry_on_unrecoverable_runtime_error: bool = Field(default=False)
|
||||
remote_runtime_class: str | None = Field(
|
||||
default=None
|
||||
) # can be "None" (default to gvisor) or "sysbox" (support docker inside runtime + more stable)
|
||||
|
||||
@@ -12,7 +12,6 @@ class BrowseURLAction(Action):
|
||||
action: str = ActionType.BROWSE
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
return_axtree: bool = False
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
@@ -34,7 +33,6 @@ class BrowseInteractiveAction(Action):
|
||||
action: str = ActionType.BROWSE_INTERACTIVE
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
return_axtree: bool = False
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from openhands.core.schema import ObservationType
|
||||
from browsergym.utils.obs import flatten_axtree_to_str
|
||||
|
||||
from openhands.core.schema import ActionType, ObservationType
|
||||
from openhands.events.observation.observation import Observation
|
||||
|
||||
|
||||
@@ -51,5 +53,69 @@ class BrowserOutputObservation(Observation):
|
||||
if self.screenshot_path:
|
||||
ret += f'Screenshot saved to: {self.screenshot_path}\n'
|
||||
ret += '--- Agent Observation ---\n'
|
||||
ret += self.content
|
||||
ret += self.get_agent_obs_text()
|
||||
return ret
|
||||
|
||||
def get_agent_obs_text(self) -> str:
|
||||
"""Get a concise text that will be shown to the agent."""
|
||||
if self.trigger_by_action == ActionType.BROWSE_INTERACTIVE:
|
||||
text = f'[Current URL: {self.url}]\n'
|
||||
text += f'[Focused element bid: {self.focused_element_bid}]\n'
|
||||
|
||||
# Add screenshot path information if available
|
||||
if self.screenshot_path:
|
||||
text += f'[Screenshot saved to: {self.screenshot_path}]\n'
|
||||
|
||||
text += '\n'
|
||||
|
||||
if self.error:
|
||||
text += (
|
||||
'================ BEGIN error message ===============\n'
|
||||
'The following error occurred when executing the last action:\n'
|
||||
f'{self.last_browser_action_error}\n'
|
||||
'================ END error message ===============\n'
|
||||
)
|
||||
else:
|
||||
text += '[Action executed successfully.]\n'
|
||||
try:
|
||||
# We do not filter visible only here because we want to show the full content
|
||||
# of the web page to the agent for simplicity.
|
||||
# FIXME: handle the case when the web page is too large
|
||||
cur_axtree_txt = self.get_axtree_str(filter_visible_only=False)
|
||||
text += (
|
||||
f'============== BEGIN accessibility tree ==============\n'
|
||||
f'{cur_axtree_txt}\n'
|
||||
f'============== END accessibility tree ==============\n'
|
||||
)
|
||||
except Exception as e:
|
||||
text += (
|
||||
f'\n[Error encountered when processing the accessibility tree: {e}]'
|
||||
)
|
||||
return text
|
||||
|
||||
elif self.trigger_by_action == ActionType.BROWSE:
|
||||
text = f'[Current URL: {self.url}]\n'
|
||||
|
||||
if self.error:
|
||||
text += (
|
||||
'================ BEGIN error message ===============\n'
|
||||
'The following error occurred when trying to visit the URL:\n'
|
||||
f'{self.last_browser_action_error}\n'
|
||||
'================ END error message ===============\n'
|
||||
)
|
||||
text += '============== BEGIN webpage content ==============\n'
|
||||
text += self.content
|
||||
text += '\n============== END webpage content ==============\n'
|
||||
return text
|
||||
else:
|
||||
raise ValueError(f'Invalid trigger_by_action: {self.trigger_by_action}')
|
||||
|
||||
def get_axtree_str(self, filter_visible_only: bool = False) -> str:
|
||||
cur_axtree_txt = flatten_axtree_to_str(
|
||||
self.axtree_object,
|
||||
extra_properties=self.extra_element_properties,
|
||||
with_clickable=True,
|
||||
skip_generic=False,
|
||||
filter_visible_only=filter_visible_only,
|
||||
)
|
||||
return str(cur_axtree_txt)
|
||||
|
||||
@@ -426,10 +426,7 @@ def convert_tool_call_to_string(tool_call: dict) -> str:
|
||||
ret += f'<parameter={param_name}>'
|
||||
if is_multiline:
|
||||
ret += '\n'
|
||||
if isinstance(param_value, list) or isinstance(param_value, dict):
|
||||
ret += json.dumps(param_value)
|
||||
else:
|
||||
ret += f'{param_value}'
|
||||
ret += f'{param_value}'
|
||||
if is_multiline:
|
||||
ret += '\n'
|
||||
ret += '</parameter>\n'
|
||||
|
||||
@@ -391,7 +391,7 @@ class ConversationMemory:
|
||||
role='user', content=[TextContent(text=obs.content)]
|
||||
) # Content is already truncated by openhands-aci
|
||||
elif isinstance(obs, BrowserOutputObservation):
|
||||
text = obs.content
|
||||
text = obs.get_agent_obs_text()
|
||||
if (
|
||||
obs.trigger_by_action == ActionType.BROWSE_INTERACTIVE
|
||||
and enable_som_visual_browsing
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
You are tasked with generating a prompt that will be used by another AI to update a special reference file. This file contains important information and learnings that are used to carry out certain tasks. The file can be extended over time to incorporate new knowledge and experiences.
|
||||
|
||||
You have been provided with a subset of new events that may require updates to the special file. These events are:
|
||||
<events>
|
||||
{{ events }}
|
||||
</events>
|
||||
|
||||
Your task is to analyze these events and determine what updates, if any, should be made to the special file. Then, you need to generate a prompt that will instruct another AI to make these updates correctly and efficiently.
|
||||
|
||||
When creating your prompt, follow these guidelines:
|
||||
1. Clearly specify which parts of the file need to be updated or if new sections should be added.
|
||||
2. Provide context for why these updates are necessary based on the new events.
|
||||
3. Be specific about the information that should be added or modified.
|
||||
4. Maintain the existing structure and formatting of the file.
|
||||
5. Ensure that the updates are consistent with the current content and don't contradict existing information.
|
||||
|
||||
Now, based on the new events provided, generate a prompt that will guide the AI in making the appropriate updates to the special file. Your prompt should be clear, specific, and actionable. Include your prompt within <update_prompt> tags.
|
||||
|
||||
<update_prompt>
|
||||
|
||||
</update_prompt>
|
||||
@@ -14,15 +14,10 @@ from typing import Callable, cast
|
||||
from zipfile import ZipFile
|
||||
|
||||
import httpx
|
||||
import tenacity
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
||||
|
||||
from openhands.core.config import OpenHandsConfig, SandboxConfig
|
||||
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig
|
||||
from openhands.core.exceptions import (
|
||||
AgentRuntimeDisconnectedError,
|
||||
AgentRuntimeUnavailableError,
|
||||
)
|
||||
from openhands.core.exceptions import AgentRuntimeDisconnectedError
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events import EventSource, EventStream, EventStreamSubscriber
|
||||
from openhands.events.action import (
|
||||
@@ -64,7 +59,6 @@ from openhands.runtime.plugins import (
|
||||
PluginRequirement,
|
||||
VSCodeRequirement,
|
||||
)
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.edit import FileEditRuntimeMixin
|
||||
from openhands.runtime.utils.git_handler import CommandResult, GitHandler
|
||||
from openhands.utils.async_utils import (
|
||||
@@ -73,6 +67,16 @@ from openhands.utils.async_utils import (
|
||||
call_sync_from_async,
|
||||
)
|
||||
|
||||
STATUS_MESSAGES = {
|
||||
'STATUS$STARTING_RUNTIME': 'Starting runtime...',
|
||||
'STATUS$STARTING_CONTAINER': 'Starting container...',
|
||||
'STATUS$PREPARING_CONTAINER': 'Preparing container...',
|
||||
'STATUS$CONTAINER_STARTED': 'Container started.',
|
||||
'STATUS$WAITING_FOR_CLIENT': 'Waiting for client...',
|
||||
'STATUS$SETTING_UP_WORKSPACE': 'Setting up workspace...',
|
||||
'STATUS$SETTING_UP_GIT_HOOKS': 'Setting up git hooks...',
|
||||
}
|
||||
|
||||
|
||||
def _default_env_vars(sandbox_config: SandboxConfig) -> dict[str, str]:
|
||||
ret = {}
|
||||
@@ -120,7 +124,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
initial_env_vars: dict[str, str]
|
||||
attach_to_existing: bool
|
||||
status_callback: Callable[[str, str, str], None] | None
|
||||
runtime_status: RuntimeStatus | None
|
||||
_runtime_initialized: bool = False
|
||||
|
||||
def __init__(
|
||||
@@ -184,7 +187,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
|
||||
self.user_id = user_id
|
||||
self.git_provider_tokens = git_provider_tokens
|
||||
self.runtime_status = None
|
||||
|
||||
@property
|
||||
def runtime_initialized(self) -> bool:
|
||||
@@ -213,12 +215,11 @@ class Runtime(FileEditRuntimeMixin):
|
||||
message = f'[runtime {self.sid}] {message}'
|
||||
getattr(logger, level)(message, stacklevel=2)
|
||||
|
||||
def set_runtime_status(self, runtime_status: RuntimeStatus):
|
||||
def send_status_message(self, message_id: str):
|
||||
"""Sends a status message if the callback function was provided."""
|
||||
self.runtime_status = runtime_status
|
||||
if self.status_callback:
|
||||
msg_id: str = runtime_status.value # type: ignore
|
||||
self.status_callback('info', msg_id, runtime_status.message)
|
||||
msg = STATUS_MESSAGES.get(message_id, '')
|
||||
self.status_callback('info', message_id, msg)
|
||||
|
||||
def send_error_message(self, message_id: str, message: str):
|
||||
if self.status_callback:
|
||||
@@ -338,134 +339,22 @@ class Runtime(FileEditRuntimeMixin):
|
||||
f'Failed export latest github token to runtime: {self.sid}, {e}'
|
||||
)
|
||||
|
||||
async def _handle_runtime_error(
|
||||
self,
|
||||
event: Action,
|
||||
error: Exception,
|
||||
retry_count: int,
|
||||
max_retries: int = 3,
|
||||
retry_delay: int = 10,
|
||||
) -> None:
|
||||
"""
|
||||
Handle runtime-related errors with retry logic.
|
||||
|
||||
Args:
|
||||
event: The action that caused the error
|
||||
error: The exception that was raised
|
||||
retry_count: Current retry attempt number
|
||||
max_retries: Maximum number of retry attempts
|
||||
retry_delay: Delay in seconds between retries
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
error_message = f'{type(error).__name__}: {str(error)}'
|
||||
self.log('error', f'Runtime error while running action: {error_message}')
|
||||
self.log('error', f'Problematic action: {str(event)}')
|
||||
|
||||
# Reset MCP stdio servers tracking when error happens
|
||||
if hasattr(self, '_last_updated_mcp_stdio_servers'):
|
||||
from openhands.core.config.mcp_config import MCPStdioServerConfig
|
||||
|
||||
self._last_updated_mcp_stdio_servers: list[MCPStdioServerConfig] = []
|
||||
self.log(
|
||||
'debug',
|
||||
'Reset _last_updated_mcp_stdio_servers to empty list due to runtime error',
|
||||
)
|
||||
|
||||
# Create error message for the observation
|
||||
error_content = (
|
||||
f'Your command may have consumed too much resources, and the previous runtime died. '
|
||||
f'You are connected to a new runtime container, all dependencies you have installed '
|
||||
f'outside /workspace may not be persisted. (Retry {retry_count} of {max_retries})'
|
||||
)
|
||||
|
||||
# Create an error observation
|
||||
observation = ErrorObservation(content=error_content)
|
||||
|
||||
# Add the observation to the event stream
|
||||
observation._cause = event.id # type: ignore[attr-defined]
|
||||
observation.tool_call_metadata = event.tool_call_metadata
|
||||
self.event_stream.add_event(observation, EventSource.ENVIRONMENT) # type: ignore[arg-type]
|
||||
|
||||
# Log the retry attempt
|
||||
self.log(
|
||||
'warning',
|
||||
f'Runtime error occurred. Retry {retry_count} of {max_retries}.',
|
||||
)
|
||||
|
||||
async def _execute_action_core(self, event: Action) -> Observation:
|
||||
"""
|
||||
Core logic for executing an action.
|
||||
|
||||
Args:
|
||||
event: The action to execute
|
||||
|
||||
Returns:
|
||||
The observation resulting from the action
|
||||
"""
|
||||
await self._export_latest_git_provider_tokens(event)
|
||||
if isinstance(event, MCPAction):
|
||||
observation: Observation = await self.call_tool_mcp(event)
|
||||
else:
|
||||
observation = await call_sync_from_async(self.run_action, event)
|
||||
return observation
|
||||
|
||||
async def _handle_action(self, event: Action) -> None:
|
||||
if event.timeout is None:
|
||||
# We don't block the command if this is a default timeout action
|
||||
event.set_hard_timeout(self.config.sandbox.timeout, blocking=False)
|
||||
assert event.timeout is not None
|
||||
|
||||
# Define a before_sleep callback for tenacity
|
||||
async def before_sleep_callback(retry_state: tenacity.RetryCallState) -> None:
|
||||
exception = retry_state.outcome.exception()
|
||||
if exception:
|
||||
await self._handle_runtime_error(
|
||||
event,
|
||||
exception,
|
||||
retry_state.attempt_number,
|
||||
max_retries=3,
|
||||
retry_delay=10,
|
||||
)
|
||||
|
||||
# Create a retry decorator based on configuration
|
||||
if self.config.sandbox.retry_on_unrecoverable_runtime_error:
|
||||
retry_decorator = retry(
|
||||
retry=retry_if_exception_type(
|
||||
(AgentRuntimeDisconnectedError, AgentRuntimeUnavailableError)
|
||||
),
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_fixed(10),
|
||||
before_sleep=before_sleep_callback,
|
||||
reraise=True,
|
||||
)
|
||||
execute_with_retry = retry_decorator(self._execute_action_core)
|
||||
else:
|
||||
# No retry if not enabled in config
|
||||
execute_with_retry = self._execute_action_core
|
||||
|
||||
try:
|
||||
# Execute the action with retry if configured
|
||||
observation: Observation = await execute_with_retry(event)
|
||||
|
||||
# Set observation metadata
|
||||
observation._cause = event.id # type: ignore[attr-defined]
|
||||
observation.tool_call_metadata = event.tool_call_metadata
|
||||
|
||||
except (AgentRuntimeDisconnectedError, AgentRuntimeUnavailableError) as e:
|
||||
# This will only be reached if retries are disabled or all retries failed
|
||||
err_id = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
|
||||
error_message = f'{type(e).__name__}: {str(e)}'
|
||||
self.log('error', f'Runtime error while running action: {error_message}')
|
||||
self.log('error', f'Problematic action: {str(event)}')
|
||||
self.send_error_message(err_id, error_message)
|
||||
return
|
||||
|
||||
await self._export_latest_git_provider_tokens(event)
|
||||
if isinstance(event, MCPAction):
|
||||
observation: Observation = await self.call_tool_mcp(event)
|
||||
else:
|
||||
observation = await call_sync_from_async(self.run_action, event)
|
||||
except Exception as e:
|
||||
# Handle other exceptions
|
||||
err_id = ''
|
||||
if isinstance(e, httpx.NetworkError):
|
||||
if isinstance(e, httpx.NetworkError) or isinstance(
|
||||
e, AgentRuntimeDisconnectedError
|
||||
):
|
||||
err_id = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
|
||||
error_message = f'{type(e).__name__}: {str(e)}'
|
||||
self.log('error', f'Unexpected error while running action: {error_message}')
|
||||
@@ -473,6 +362,9 @@ class Runtime(FileEditRuntimeMixin):
|
||||
self.send_error_message(err_id, error_message)
|
||||
return
|
||||
|
||||
observation._cause = event.id # type: ignore[attr-defined]
|
||||
observation.tool_call_metadata = event.tool_call_metadata
|
||||
|
||||
# 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):
|
||||
|
||||
@@ -2,9 +2,7 @@ import base64
|
||||
import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from browsergym.utils.obs import flatten_axtree_to_str
|
||||
from PIL import Image
|
||||
|
||||
from openhands.core.exceptions import BrowserUnavailableException
|
||||
@@ -16,78 +14,6 @@ from openhands.runtime.browser.browser_env import BrowserEnv
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
|
||||
def get_axtree_str(
|
||||
axtree_object: dict[str, Any],
|
||||
extra_element_properties: dict[str, Any],
|
||||
filter_visible_only: bool = False,
|
||||
) -> str:
|
||||
cur_axtree_txt = flatten_axtree_to_str(
|
||||
axtree_object,
|
||||
extra_properties=extra_element_properties,
|
||||
with_clickable=True,
|
||||
skip_generic=False,
|
||||
filter_visible_only=filter_visible_only,
|
||||
)
|
||||
return str(cur_axtree_txt)
|
||||
|
||||
|
||||
def get_agent_obs_text(obs: BrowserOutputObservation) -> str:
|
||||
"""Get a concise text that will be shown to the agent."""
|
||||
if obs.trigger_by_action == ActionType.BROWSE_INTERACTIVE:
|
||||
text = f'[Current URL: {obs.url}]\n'
|
||||
text += f'[Focused element bid: {obs.focused_element_bid}]\n'
|
||||
|
||||
# Add screenshot path information if available
|
||||
if obs.screenshot_path:
|
||||
text += f'[Screenshot saved to: {obs.screenshot_path}]\n'
|
||||
|
||||
text += '\n'
|
||||
|
||||
if obs.error:
|
||||
text += (
|
||||
'================ BEGIN error message ===============\n'
|
||||
'The following error occurred when executing the last action:\n'
|
||||
f'{obs.last_browser_action_error}\n'
|
||||
'================ END error message ===============\n'
|
||||
)
|
||||
else:
|
||||
text += '[Action executed successfully.]\n'
|
||||
try:
|
||||
# We do not filter visible only here because we want to show the full content
|
||||
# of the web page to the agent for simplicity.
|
||||
# FIXME: handle the case when the web page is too large
|
||||
cur_axtree_txt = get_axtree_str(
|
||||
obs.axtree_object,
|
||||
obs.extra_element_properties,
|
||||
filter_visible_only=False,
|
||||
)
|
||||
text += (
|
||||
f'============== BEGIN accessibility tree ==============\n'
|
||||
f'{cur_axtree_txt}\n'
|
||||
f'============== END accessibility tree ==============\n'
|
||||
)
|
||||
except Exception as e:
|
||||
text += f'\n[Error encountered when processing the accessibility tree: {e}]'
|
||||
return text
|
||||
|
||||
elif obs.trigger_by_action == ActionType.BROWSE:
|
||||
text = f'[Current URL: {obs.url}]\n'
|
||||
|
||||
if obs.error:
|
||||
text += (
|
||||
'================ BEGIN error message ===============\n'
|
||||
'The following error occurred when trying to visit the URL:\n'
|
||||
f'{obs.last_browser_action_error}\n'
|
||||
'================ END error message ===============\n'
|
||||
)
|
||||
text += '============== BEGIN webpage content ==============\n'
|
||||
text += obs.content
|
||||
text += '\n============== END webpage content ==============\n'
|
||||
return text
|
||||
else:
|
||||
raise ValueError(f'Invalid trigger_by_action: {obs.trigger_by_action}')
|
||||
|
||||
|
||||
async def browse(
|
||||
action: BrowseURLAction | BrowseInteractiveAction,
|
||||
browser: BrowserEnv | None,
|
||||
@@ -152,8 +78,7 @@ async def browse(
|
||||
image = png_base64_url_to_image(obs.get('screenshot'))
|
||||
image.save(screenshot_path, format='PNG', optimize=True)
|
||||
|
||||
# Create the observation with all data
|
||||
observation = BrowserOutputObservation(
|
||||
return BrowserOutputObservation(
|
||||
content=obs['text_content'], # text content of the page
|
||||
url=obs.get('url', ''), # URL of the page
|
||||
screenshot=obs.get('screenshot', None), # base64-encoded screenshot, png
|
||||
@@ -178,37 +103,13 @@ async def browse(
|
||||
error=True if obs.get('last_action_error', '') else False, # error flag
|
||||
trigger_by_action=action.action,
|
||||
)
|
||||
|
||||
# Process the content first using the axtree_object
|
||||
observation.content = get_agent_obs_text(observation)
|
||||
|
||||
# If return_axtree is False, remove the axtree_object to save space
|
||||
if not action.return_axtree:
|
||||
observation.dom_object = {}
|
||||
observation.axtree_object = {}
|
||||
observation.extra_element_properties = {}
|
||||
|
||||
return observation
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
error_url = asked_url if action.action == ActionType.BROWSE else ''
|
||||
|
||||
# Create error observation
|
||||
observation = BrowserOutputObservation(
|
||||
content=error_message,
|
||||
return BrowserOutputObservation(
|
||||
content=str(e),
|
||||
screenshot='',
|
||||
screenshot_path=None,
|
||||
error=True,
|
||||
last_browser_action_error=error_message,
|
||||
url=error_url,
|
||||
last_browser_action_error=str(e),
|
||||
url=asked_url if action.action == ActionType.BROWSE else '',
|
||||
trigger_by_action=action.action,
|
||||
)
|
||||
|
||||
# Process the content using get_agent_obs_text regardless of return_axtree value
|
||||
try:
|
||||
observation.content = get_agent_obs_text(observation)
|
||||
except Exception:
|
||||
# If get_agent_obs_text fails, keep the original error message
|
||||
pass
|
||||
|
||||
return observation
|
||||
|
||||
@@ -49,7 +49,6 @@ from openhands.events.observation import (
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
|
||||
|
||||
class CLIRuntime(Runtime):
|
||||
@@ -127,7 +126,7 @@ class CLIRuntime(Runtime):
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Initialize the runtime connection."""
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
|
||||
# Ensure workspace directory exists
|
||||
os.makedirs(self._workspace_path, exist_ok=True)
|
||||
@@ -139,7 +138,7 @@ class CLIRuntime(Runtime):
|
||||
await asyncio.to_thread(self.setup_initial_env)
|
||||
|
||||
self._runtime_initialized = True
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
self.send_status_message('STATUS$CONTAINER_STARTED')
|
||||
logger.info(f'CLIRuntime initialized with workspace at {self._workspace_path}')
|
||||
|
||||
def add_env_vars(self, env_vars: dict[str, Any]) -> None:
|
||||
|
||||
@@ -17,7 +17,6 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins.requirement import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.command import get_action_execution_server_startup_command
|
||||
from openhands.runtime.utils.request import RequestHTTPError
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
@@ -171,7 +170,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
super().check_if_alive()
|
||||
|
||||
async def connect(self):
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
should_start_action_execution_server = False
|
||||
|
||||
if self.attach_to_existing:
|
||||
@@ -180,7 +179,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
should_start_action_execution_server = True
|
||||
|
||||
if self.workspace is None:
|
||||
self.set_runtime_status(RuntimeStatus.BUILDING_RUNTIME)
|
||||
self.send_status_message('STATUS$PREPARING_CONTAINER')
|
||||
self.workspace = await call_sync_from_async(self._create_workspace)
|
||||
self.log('info', f'Created new workspace with id: {self.workspace_id}')
|
||||
|
||||
@@ -206,7 +205,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
self.log('info', 'Waiting for client to become ready...')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
await call_sync_from_async(self._wait_until_alive)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
@@ -218,7 +217,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self.send_status_message(' ')
|
||||
self._runtime_initialized = True
|
||||
|
||||
@tenacity.retry(
|
||||
|
||||
@@ -23,7 +23,6 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
)
|
||||
from openhands.runtime.impl.docker.containers import stop_all_containers
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils import find_available_tcp_port
|
||||
from openhands.runtime.utils.command import (
|
||||
DEFAULT_MAIN_MODULE,
|
||||
@@ -146,7 +145,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
return self.api_url
|
||||
|
||||
async def connect(self) -> None:
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
try:
|
||||
await call_sync_from_async(self._attach_to_container)
|
||||
except docker.errors.NotFound as e:
|
||||
@@ -173,7 +172,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.log('info', f'Waiting for client to become ready at {self.api_url}...')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
|
||||
await call_sync_from_async(self.wait_until_alive)
|
||||
|
||||
@@ -188,7 +187,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}. VSCode URL: {self.vscode_url}',
|
||||
)
|
||||
if not self.attach_to_existing:
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self.send_status_message(' ')
|
||||
self._runtime_initialized = True
|
||||
|
||||
def maybe_build_runtime_container_image(self):
|
||||
@@ -197,7 +196,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
raise ValueError(
|
||||
'Neither runtime container image nor base container image is set'
|
||||
)
|
||||
self.set_runtime_status(RuntimeStatus.BUILDING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_CONTAINER')
|
||||
self.runtime_container_image = build_runtime_image(
|
||||
self.base_container_image,
|
||||
self.runtime_builder,
|
||||
@@ -268,7 +267,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
|
||||
def init_container(self) -> None:
|
||||
self.log('debug', 'Preparing to start container...')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$PREPARING_CONTAINER')
|
||||
self._host_port = self._find_available_port(EXECUTION_SERVER_PORT_RANGE)
|
||||
self._container_port = self._host_port
|
||||
# Use the configured vscode_port if provided, otherwise find an available port
|
||||
@@ -377,7 +376,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
**(self.config.sandbox.docker_runtime_kwargs or {}),
|
||||
)
|
||||
self.log('debug', f'Container started. Server url: {self.api_url}')
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
self.send_status_message('STATUS$CONTAINER_STARTED')
|
||||
except Exception as e:
|
||||
self.log(
|
||||
'error',
|
||||
|
||||
@@ -35,7 +35,6 @@ from openhands.runtime.impl.docker.docker_runtime import (
|
||||
VSCODE_PORT_RANGE,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils import find_available_tcp_port
|
||||
from openhands.runtime.utils.command import get_action_execution_server_startup_command
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
@@ -207,7 +206,7 @@ class LocalRuntime(ActionExecutionClient):
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Start the action_execution_server on the local machine or connect to an existing one."""
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
|
||||
# Check if there's already a server running for this session ID
|
||||
if self.sid in _RUNNING_SERVERS:
|
||||
@@ -384,7 +383,7 @@ class LocalRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
self.log('info', f'Waiting for server to become ready at {self.api_url}...')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
|
||||
await call_sync_from_async(self._wait_until_alive)
|
||||
|
||||
@@ -396,7 +395,7 @@ class LocalRuntime(ActionExecutionClient):
|
||||
f'Server initialized with plugins: {[plugin.name for plugin in self.plugins]}',
|
||||
)
|
||||
if not self.attach_to_existing:
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self.send_status_message(' ')
|
||||
self._runtime_initialized = True
|
||||
|
||||
def _find_available_port(
|
||||
|
||||
@@ -13,7 +13,6 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.command import get_action_execution_server_startup_command
|
||||
from openhands.runtime.utils.runtime_build import (
|
||||
BuildFromImageType,
|
||||
@@ -103,7 +102,7 @@ class ModalRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
async def connect(self):
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
|
||||
self.log('debug', f'ModalRuntime `{self.sid}`')
|
||||
|
||||
@@ -121,14 +120,14 @@ class ModalRuntime(ActionExecutionClient):
|
||||
sandbox_id, client=self.modal_client
|
||||
)
|
||||
else:
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$PREPARING_CONTAINER')
|
||||
await call_sync_from_async(
|
||||
self._init_sandbox,
|
||||
sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,
|
||||
plugins=self.plugins,
|
||||
)
|
||||
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
self.send_status_message('STATUS$CONTAINER_STARTED')
|
||||
|
||||
if self.sandbox is None:
|
||||
raise Exception('Sandbox not initialized')
|
||||
@@ -138,13 +137,13 @@ class ModalRuntime(ActionExecutionClient):
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.log('debug', 'Waiting for client to become ready...')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
|
||||
self._wait_until_alive()
|
||||
self.setup_initial_env()
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self.send_status_message(' ')
|
||||
self._runtime_initialized = True
|
||||
|
||||
@property
|
||||
|
||||
@@ -24,7 +24,6 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.command import (
|
||||
DEFAULT_MAIN_MODULE,
|
||||
get_action_execution_server_startup_command,
|
||||
@@ -140,7 +139,7 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
)
|
||||
else:
|
||||
self.log('info', 'No existing runtime found, starting a new one')
|
||||
self.set_runtime_status(RuntimeStatus.BUILDING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_CONTAINER')
|
||||
if self.config.sandbox.runtime_container_image is None:
|
||||
self.log(
|
||||
'info',
|
||||
@@ -160,13 +159,13 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
assert self.runtime_url is not None, (
|
||||
'Runtime URL is not set. This should never happen.'
|
||||
)
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
if not self.attach_to_existing:
|
||||
self.log('info', 'Waiting for runtime to be alive...')
|
||||
self._wait_until_alive()
|
||||
if not self.attach_to_existing:
|
||||
self.log('info', 'Runtime is ready.')
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self.send_status_message(' ')
|
||||
|
||||
def _check_existing_runtime(self) -> bool:
|
||||
self.log('info', f'Checking for existing runtime with session ID: {self.sid}')
|
||||
@@ -308,7 +307,7 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
4. Update env vars
|
||||
"""
|
||||
self.log('info', f'Attempting to resume runtime with ID: {self.runtime_id}')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
try:
|
||||
response = self._send_runtime_api_request(
|
||||
'POST',
|
||||
|
||||