Compare commits
28 Commits
docs/updat
...
add-tmp-di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ff269a073 | ||
|
|
e6036b8346 | ||
|
|
144d09a578 | ||
|
|
f97a837d46 | ||
|
|
eadec4ce9e | ||
|
|
49e8737779 | ||
|
|
4711e74101 | ||
|
|
c87f1cc8c0 | ||
|
|
33b64786b0 | ||
|
|
12fc50299b | ||
|
|
57fee17348 | ||
|
|
77517d8ba0 | ||
|
|
a356f56237 | ||
|
|
7dede37fd8 | ||
|
|
c11dcad309 | ||
|
|
47209e794a | ||
|
|
3f50eb0079 | ||
|
|
f27b02411b | ||
|
|
d151093872 | ||
|
|
ea7294b7f9 | ||
|
|
9097f487a6 | ||
|
|
fd921a4f88 | ||
|
|
96fe5a50d6 | ||
|
|
b634e10b45 | ||
|
|
73f01657eb | ||
|
|
5d328183d5 | ||
|
|
b7da65d373 | ||
|
|
dca9c7bdc6 |
@@ -2,6 +2,8 @@ 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.
|
||||
|
||||
@@ -19,91 +21,13 @@ 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
|
||||
@@ -120,19 +44,18 @@ 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.
|
||||
@@ -163,4 +86,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.41-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.43-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
17
README.md
@@ -18,6 +18,17 @@
|
||||
<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>
|
||||
|
||||
@@ -51,17 +62,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-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.41
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
```
|
||||
|
||||
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.41-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-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.41
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
```
|
||||
|
||||
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands!
|
||||
|
||||
@@ -11,11 +11,12 @@ 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.41-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.43-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.41-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.43-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:
|
||||
|
||||
17
docs/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Setup
|
||||
|
||||
```
|
||||
npm install -g mint
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
yarn global add mint
|
||||
```
|
||||
|
||||
# Preview
|
||||
|
||||
```
|
||||
mint dev
|
||||
```
|
||||
104
docs/docs.json
@@ -34,7 +34,8 @@
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
"usage/cloud/github-installation",
|
||||
"usage/cloud/gitlab-installation"
|
||||
"usage/cloud/gitlab-installation",
|
||||
"usage/cloud/slack-installation"
|
||||
]
|
||||
},
|
||||
"usage/cloud/cloud-ui",
|
||||
@@ -48,13 +49,62 @@
|
||||
"usage/how-to/gui-mode",
|
||||
"usage/how-to/cli-mode",
|
||||
"usage/how-to/headless-mode",
|
||||
"usage/how-to/github-action"
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Customization",
|
||||
"pages": [
|
||||
"usage/prompting/prompting-best-practices",
|
||||
"usage/prompting/repository",
|
||||
{
|
||||
"group": "Microagents",
|
||||
@@ -69,53 +119,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Advanced Configuration",
|
||||
"group": "Tips and Tricks",
|
||||
"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/prompting/prompting-best-practices"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
BIN
docs/static/img/slack-create-convo.png
vendored
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/static/img/slack-pro-tip.png
vendored
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
docs/static/img/slack-results-and-follow-up.png
vendored
Normal file
|
After Width: | Height: | Size: 542 KiB |
@@ -19,6 +19,12 @@ 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).
|
||||
|
||||
52
docs/usage/cloud/slack-installation.mdx
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
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.41-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-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.41 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43 \
|
||||
python -m openhands.cli.main --override-cli-mode true
|
||||
```
|
||||
|
||||
|
||||
@@ -100,6 +100,11 @@ 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">
|
||||
@@ -132,6 +137,20 @@ 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.41-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-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.41 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -14,23 +14,28 @@ 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>
|
||||
|
||||
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!
|
||||
### Local / Self-Hosted Models
|
||||
|
||||
For a full list of the providers and models available, please consult the
|
||||
[litellm documentation](https://docs.litellm.ai/docs/providers).
|
||||
- [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)
|
||||
|
||||
<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.41-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-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.41-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-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.41
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
```
|
||||
|
||||
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.41
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
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.41-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-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.41
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
@@ -117,10 +117,24 @@ 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"
|
||||
status="RUNNING"
|
||||
conversationStatus="RUNNING"
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ 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,
|
||||
},
|
||||
@@ -60,6 +61,7 @@ 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,
|
||||
},
|
||||
@@ -72,6 +74,7 @@ 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,
|
||||
},
|
||||
@@ -158,6 +161,7 @@ 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,
|
||||
},
|
||||
@@ -170,6 +174,7 @@ 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,
|
||||
},
|
||||
@@ -182,6 +187,7 @@ 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,6 +65,7 @@ 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,10 +334,7 @@ describe("Settings 404", () => {
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
// small hack to wait for the modal to not appear
|
||||
await expect(
|
||||
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
|
||||
).rejects.toThrow();
|
||||
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
18
frontend/hero.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { heroui } from "@heroui/react";
|
||||
|
||||
export default heroui({
|
||||
defaultTheme: "dark",
|
||||
layout: {
|
||||
radius: {
|
||||
small: "5px",
|
||||
large: "20px",
|
||||
},
|
||||
},
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
primary: "#4465DB",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
6602
frontend/package-lock.json
generated
@@ -1,37 +1,39 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.41.0",
|
||||
"version": "0.43.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.8",
|
||||
"@heroui/react": "^2.8.0-beta.7",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.6.1",
|
||||
"@react-router/serve": "^7.6.1",
|
||||
"@react-router/node": "^7.6.2",
|
||||
"@react-router/serve": "^7.6.2",
|
||||
"@react-types/shared": "^3.29.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.0",
|
||||
"@tanstack/react-query": "^5.77.2",
|
||||
"@vitejs/plugin-react": "^4.4.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",
|
||||
"@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.14.0",
|
||||
"framer-motion": "^12.17.3",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.28",
|
||||
"jose": "^6.0.11",
|
||||
"lucide-react": "^0.511.0",
|
||||
"lucide-react": "^0.514.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.245.2",
|
||||
"posthog-js": "^1.251.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -40,15 +42,15 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.6.1",
|
||||
"react-router": "^7.6.2",
|
||||
"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.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vite": "^6.3.5",
|
||||
"web-vitals": "^5.0.1",
|
||||
"web-vitals": "^5.0.3",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -82,23 +84,23 @@
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@react-router/dev": "^7.6.1",
|
||||
"@playwright/test": "^1.53.0",
|
||||
"@react-router/dev": "^7.6.2",
|
||||
"@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": "^22.15.21",
|
||||
"@types/react": "^19.1.5",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/node": "^24.0.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@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.1.4",
|
||||
"@vitest/coverage-v8": "^3.2.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -113,12 +115,11 @@
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"lint-staged": "^16.1.0",
|
||||
"msw": "^2.6.6",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"stripe": "^18.1.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"stripe": "^18.2.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,6 +117,9 @@ 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,6 +11,8 @@ import {
|
||||
GetTrajectoryResponse,
|
||||
GitChangeDiff,
|
||||
GitChange,
|
||||
GetMicroagentsResponse,
|
||||
GetMicroagentPromptResponse,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
|
||||
@@ -393,6 +395,35 @@ 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,4 +1,5 @@
|
||||
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { RuntimeStatus } from "#/types/runtime-status";
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
@@ -80,7 +81,8 @@ export interface Conversation {
|
||||
git_provider: string | null;
|
||||
last_updated_at: string;
|
||||
created_at: string;
|
||||
status: ProjectStatus;
|
||||
status: ConversationStatus;
|
||||
runtime_status: RuntimeStatus | null;
|
||||
trigger?: ConversationTrigger;
|
||||
url: string | null;
|
||||
session_api_key: string | null;
|
||||
@@ -102,3 +104,24 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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-none ring-0",
|
||||
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none outline-hidden 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 flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
|
||||
className="mt-2 mb-2 w-full h-10 rounded-sm flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
|
||||
to="/settings/billing"
|
||||
>
|
||||
{t(I18nKey.BILLING$CLICK_TO_TOP_UP)}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
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,
|
||||
@@ -24,39 +20,61 @@ export function AgentStatusBar() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curStatusMessage } = useSelector((state: RootState) => state.status);
|
||||
const { status } = useWsClient();
|
||||
const { notify } = useNotification();
|
||||
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 { notify } = useNotification();
|
||||
|
||||
const [statusMessage, setStatusMessage] = React.useState<string>("");
|
||||
|
||||
const updateStatusMessage = () => {
|
||||
// Show error toast if required
|
||||
React.useEffect(() => {
|
||||
if (curStatusMessage?.type !== "error") {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (curStatusMessage?.type === "error") {
|
||||
showErrorToast({
|
||||
message,
|
||||
source: "agent-status",
|
||||
metadata: { ...curStatusMessage },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (message.trim()) {
|
||||
setStatusMessage(message);
|
||||
} else {
|
||||
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
updateStatusMessage();
|
||||
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,
|
||||
});
|
||||
|
||||
// Update browser tab if window exists and is not focused
|
||||
if (typeof document !== "undefined" && !document.hasFocus()) {
|
||||
browserTab.startNotification(message);
|
||||
}
|
||||
}
|
||||
}, [curAgentState, statusCode]);
|
||||
|
||||
// Handle window focus/blur
|
||||
React.useEffect(() => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
@@ -72,45 +90,13 @@ 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(statusMessage)}</span>
|
||||
<span className="text-sm text-stone-400">{t(statusCode)}</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}
|
||||
status={conversation?.status}
|
||||
conversationStatus={conversation?.status}
|
||||
conversationId={conversation?.conversation_id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
@@ -9,6 +11,7 @@ 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";
|
||||
}
|
||||
@@ -19,9 +22,11 @@ export function ConversationCardContextMenu({
|
||||
onEdit,
|
||||
onDisplayCost,
|
||||
onShowAgentTools,
|
||||
onShowMicroagents,
|
||||
onDownloadViaVSCode,
|
||||
position = "bottom",
|
||||
}: ConversationCardContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
|
||||
return (
|
||||
@@ -68,6 +73,14 @@ export function ConversationCardContextMenu({
|
||||
Show Agent Tools & Metadata
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onShowMicroagents && (
|
||||
<ContextMenuListItem
|
||||
testId="show-microagents-button"
|
||||
onClick={onShowMicroagents}
|
||||
>
|
||||
{t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@ import posthog from "posthog-js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import {
|
||||
ProjectStatus,
|
||||
ConversationStateIndicator,
|
||||
} from "./conversation-state-indicator";
|
||||
import { 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";
|
||||
@@ -19,6 +17,7 @@ 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;
|
||||
@@ -30,7 +29,7 @@ interface ConversationCardProps {
|
||||
selectedRepository: string | null;
|
||||
lastUpdatedAt: string; // ISO 8601
|
||||
createdAt?: string; // ISO 8601
|
||||
status?: ProjectStatus;
|
||||
conversationStatus?: ConversationStatus;
|
||||
variant?: "compact" | "default";
|
||||
conversationId?: string; // Optional conversation ID for VS Code URL
|
||||
}
|
||||
@@ -49,7 +48,7 @@ export function ConversationCard({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
lastUpdatedAt,
|
||||
createdAt,
|
||||
status = "STOPPED",
|
||||
conversationStatus = "STOPPED",
|
||||
variant = "default",
|
||||
conversationId,
|
||||
}: ConversationCardProps) {
|
||||
@@ -59,6 +58,8 @@ 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);
|
||||
@@ -142,6 +143,13 @@ export function ConversationCard({
|
||||
setSystemModalVisible(true);
|
||||
};
|
||||
|
||||
const handleShowMicroagents = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
setMicroagentsModalVisible(true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (titleMode === "edit") {
|
||||
inputRef.current?.focus();
|
||||
@@ -196,7 +204,9 @@ export function ConversationCard({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<ConversationStateIndicator status={status} />
|
||||
<ConversationStateIndicator
|
||||
conversationStatus={conversationStatus}
|
||||
/>
|
||||
{hasContextMenu && (
|
||||
<div className="pl-2">
|
||||
<EllipsisButton
|
||||
@@ -225,6 +235,11 @@ export function ConversationCard({
|
||||
? handleShowAgentTools
|
||||
: undefined
|
||||
}
|
||||
onShowMicroagents={
|
||||
showOptions && conversationId
|
||||
? handleShowMicroagents
|
||||
: undefined
|
||||
}
|
||||
position={variant === "compact" ? "top" : "bottom"}
|
||||
/>
|
||||
)}
|
||||
@@ -367,6 +382,13 @@ 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}
|
||||
status={project.status}
|
||||
conversationStatus={project.status}
|
||||
conversationId={project.conversation_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
import ColdIcon from "./state-indicators/cold.svg?react";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
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";
|
||||
|
||||
type ProjectStatusWithIcon = Exclude<
|
||||
ProjectStatus,
|
||||
"CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||
>;
|
||||
|
||||
const INDICATORS: Record<ProjectStatusWithIcon, SVGIcon> = {
|
||||
STOPPED: ColdIcon,
|
||||
const CONVERSATION_STATUS_INDICATORS: Record<ConversationStatus, SVGIcon> = {
|
||||
STOPPED: StoppedIcon,
|
||||
RUNNING: RunningIcon,
|
||||
STARTING: ColdIcon,
|
||||
STARTING: StartingIcon,
|
||||
};
|
||||
|
||||
interface ConversationStateIndicatorProps {
|
||||
status: ProjectStatus;
|
||||
conversationStatus: ConversationStatus;
|
||||
}
|
||||
|
||||
export function ConversationStateIndicator({
|
||||
status,
|
||||
conversationStatus,
|
||||
}: ConversationStateIndicatorProps) {
|
||||
// @ts-expect-error - Type 'ProjectStatus' is not assignable to type 'ProjectStatusWithIcon'.
|
||||
const StateIcon = INDICATORS[status];
|
||||
const StateIcon = CONVERSATION_STATUS_INDICATORS[conversationStatus];
|
||||
|
||||
return (
|
||||
<div data-testid={`${status}-indicator`}>
|
||||
<div data-testid={`${conversationStatus}-indicator`}>
|
||||
<StateIcon />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 968 B |
|
Before Width: | Height: | Size: 904 B After Width: | Height: | Size: 904 B |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 726 B |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before 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"
|
||||
className="bg-[#27272A] px-3 py-[10px] rounded-sm"
|
||||
/>
|
||||
</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 text-red-500"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm 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"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
|
||||
>
|
||||
<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 text-red-500"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm 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"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
|
||||
>
|
||||
<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 object-cover",
|
||||
"rounded-sm 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 px-3 py-2",
|
||||
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded-sm 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 disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
|
||||
"w-fit p-2 text-sm rounded-sm 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-none"
|
||||
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-hidden"
|
||||
value={configText}
|
||||
onChange={handleTextChange}
|
||||
spellCheck="false"
|
||||
|
||||
@@ -158,7 +158,7 @@ export function SecretForm({
|
||||
required
|
||||
className={cn(
|
||||
"resize-none",
|
||||
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"bg-tertiary border border-[#717888] rounded-sm 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 p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"bg-tertiary border border-[#717888] rounded-sm 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 p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm 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 p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm 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 px-2 py-1 cursor-pointer">
|
||||
<span className="border-2 border-dashed border-neutral-600 rounded-sm 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 w-20",
|
||||
"text-sm py-0.5 rounded-sm 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",
|
||||
variant === "default" && "text-sm font-[500] py-[10px] rounded-sm",
|
||||
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"
|
||||
className="bg-[#27272A] text-xs py-[10px] px-3 rounded-sm"
|
||||
/>
|
||||
</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 h-full"
|
||||
className="bg-neutral-500 px-1 rounded-sm 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 ${activeSection === "logs" && "bg-neutral-600"}`}
|
||||
className={`cursor-pointer p-2 rounded-sm ${activeSection === "logs" && "bg-neutral-600"}`}
|
||||
onClick={() => setActiveSection("logs")}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$LOG_LABEL)}
|
||||
</div>
|
||||
<div
|
||||
className={`cursor-pointer p-2 rounded ${activeSection === "policy" && "bg-neutral-600"}`}
|
||||
className={`cursor-pointer p-2 rounded-sm ${activeSection === "policy" && "bg-neutral-600"}`}
|
||||
onClick={() => setActiveSection("policy")}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$POLICY_LABEL)}
|
||||
</div>
|
||||
<div
|
||||
className={`cursor-pointer p-2 rounded ${activeSection === "settings" && "bg-neutral-600"}`}
|
||||
className={`cursor-pointer p-2 rounded-sm ${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 p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -142,7 +142,7 @@ export function ModelSelector({
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
} from "#/types/core/guards";
|
||||
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
|
||||
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
|
||||
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
|
||||
|
||||
export type WebSocketStatus = "CONNECTING" | "CONNECTED" | "DISCONNECTED";
|
||||
|
||||
const hasValidMessageProperty = (obj: unknown): obj is { message: string } =>
|
||||
typeof obj === "object" &&
|
||||
@@ -69,7 +70,7 @@ const isMessageAction = (
|
||||
isUserMessage(event) || isAssistantMessage(event);
|
||||
|
||||
interface UseWsClient {
|
||||
status: ProjectStatus;
|
||||
webSocketStatus: WebSocketStatus;
|
||||
isLoadingMessages: boolean;
|
||||
events: Record<string, unknown>[];
|
||||
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
|
||||
@@ -77,7 +78,7 @@ interface UseWsClient {
|
||||
}
|
||||
|
||||
const WsClientContext = React.createContext<UseWsClient>({
|
||||
status: "DISCONNECTED",
|
||||
webSocketStatus: "DISCONNECTED",
|
||||
isLoadingMessages: true,
|
||||
events: [],
|
||||
parsedEvents: [],
|
||||
@@ -134,7 +135,8 @@ export function WsClientProvider({
|
||||
const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
|
||||
const queryClient = useQueryClient();
|
||||
const sioRef = React.useRef<Socket | null>(null);
|
||||
const [status, setStatus] = React.useState<ProjectStatus>("CONNECTING");
|
||||
const [webSocketStatus, setWebSocketStatus] =
|
||||
React.useState<WebSocketStatus>("DISCONNECTED");
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
const [parsedEvents, setParsedEvents] = React.useState<
|
||||
(OpenHandsAction | OpenHandsObservation)[]
|
||||
@@ -155,7 +157,7 @@ export function WsClientProvider({
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
setStatus("CONNECTED");
|
||||
setWebSocketStatus("CONNECTED");
|
||||
removeErrorMessage();
|
||||
}
|
||||
|
||||
@@ -254,7 +256,7 @@ export function WsClientProvider({
|
||||
}
|
||||
|
||||
function handleDisconnect(data: unknown) {
|
||||
setStatus("DISCONNECTED");
|
||||
setWebSocketStatus("DISCONNECTED");
|
||||
const sio = sioRef.current;
|
||||
if (!sio) {
|
||||
return;
|
||||
@@ -268,7 +270,7 @@ export function WsClientProvider({
|
||||
|
||||
function handleError(data: unknown) {
|
||||
// set status
|
||||
setStatus("DISCONNECTED");
|
||||
setWebSocketStatus("DISCONNECTED");
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
|
||||
setErrorMessage(
|
||||
@@ -287,17 +289,14 @@ export function WsClientProvider({
|
||||
// reset events when conversationId changes
|
||||
setEvents([]);
|
||||
setParsedEvents([]);
|
||||
setStatus("CONNECTING");
|
||||
setWebSocketStatus("CONNECTING");
|
||||
}, [conversationId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!conversationId) {
|
||||
throw new Error("No conversation ID provided");
|
||||
}
|
||||
if (
|
||||
!conversation ||
|
||||
["STOPPED", "STARTING"].includes(conversation.status)
|
||||
) {
|
||||
if (conversation?.status !== "RUNNING" && !conversation?.runtime_status) {
|
||||
return () => undefined; // conversation not yet loaded
|
||||
}
|
||||
|
||||
@@ -307,6 +306,9 @@ export function WsClientProvider({
|
||||
sio.disconnect();
|
||||
}
|
||||
|
||||
// Set initial status...
|
||||
setWebSocketStatus("CONNECTING");
|
||||
|
||||
const lastEvent = lastEventRef.current;
|
||||
const query = {
|
||||
latest_event_id: lastEvent?.id ?? -1,
|
||||
@@ -341,7 +343,12 @@ export function WsClientProvider({
|
||||
sio.off("connect_failed", handleError);
|
||||
sio.off("disconnect", handleDisconnect);
|
||||
};
|
||||
}, [conversationId, conversation?.url, conversation?.status]);
|
||||
}, [
|
||||
conversationId,
|
||||
conversation?.url,
|
||||
conversation?.status,
|
||||
conversation?.runtime_status,
|
||||
]);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
@@ -356,13 +363,18 @@ export function WsClientProvider({
|
||||
|
||||
const value = React.useMemo<UseWsClient>(
|
||||
() => ({
|
||||
status,
|
||||
webSocketStatus,
|
||||
isLoadingMessages: messageRateHandler.isUnderThreshold,
|
||||
events,
|
||||
parsedEvents,
|
||||
send,
|
||||
}),
|
||||
[status, messageRateHandler.isUnderThreshold, events, parsedEvents],
|
||||
[
|
||||
webSocketStatus,
|
||||
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: status !== "DISCONNECTED" && !!conversationId,
|
||||
enabled: runtimeIsReady && !!conversationId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
25
frontend/src/hooks/query/use-conversation-microagents.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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
|
||||
});
|
||||
12
frontend/src/hooks/query/use-get-microagent-prompt.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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,15 +1,5 @@
|
||||
// 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",
|
||||
@@ -258,6 +248,9 @@ 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",
|
||||
@@ -376,10 +369,8 @@ 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",
|
||||
@@ -470,7 +461,7 @@ export enum I18nKey {
|
||||
CONVERSATION$NO_CONVERSATIONS = "CONVERSATION$NO_CONVERSATIONS",
|
||||
LANDING$SELECT_GIT_REPO = "LANDING$SELECT_GIT_REPO",
|
||||
BUTTON$SEND = "BUTTON$SEND",
|
||||
STATUS$WAITING_FOR_CLIENT = "STATUS$WAITING_FOR_CLIENT",
|
||||
STATUS$BUILDING_RUNTIME = "STATUS$BUILDING_RUNTIME",
|
||||
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",
|
||||
@@ -554,6 +545,16 @@ 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,21 +3967,69 @@
|
||||
"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": "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": "Запуск!"
|
||||
"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": "Ініціалізація агента..."
|
||||
},
|
||||
"CHAT_INTERFACE$AGENT_INIT_MESSAGE": {
|
||||
"en": "Agent is initialized, waiting for task...",
|
||||
@@ -5887,54 +5935,6 @@
|
||||
"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$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": "Чекаємо на готовність клієнта..."
|
||||
"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": "Створення середовища виконання..."
|
||||
},
|
||||
"SUGGESTIONS$WHAT_TO_BUILD": {
|
||||
"en": "What do you want to build?",
|
||||
@@ -8704,20 +8704,180 @@
|
||||
"tr": "Hizmet Şartlarını kabul ederken hata oluştu"
|
||||
},
|
||||
"TIPS$CUSTOMIZE_MICROAGENT": {
|
||||
"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."
|
||||
"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": "Не вдалося отримати мікроагентів. Будь ласка, спробуйте пізніше."
|
||||
},
|
||||
"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,6 +57,7 @@ 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,
|
||||
},
|
||||
@@ -72,6 +73,7 @@ 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,
|
||||
},
|
||||
@@ -87,6 +89,7 @@ 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,
|
||||
},
|
||||
@@ -282,6 +285,7 @@ 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,6 +19,7 @@ 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 border ${
|
||||
className={`text-base text-white p-2 bg-base-tertiary rounded-sm border ${
|
||||
isEmailChanged && !isEmailValid
|
||||
? "border-red-500"
|
||||
: "border-tertiary"
|
||||
} flex-grow focus:outline-none focus:border-transparent focus:ring-0`}
|
||||
} flex-grow focus:outline-hidden 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 bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
|
||||
className="px-4 py-2 rounded-sm 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 bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
|
||||
className="px-4 py-2 rounded-sm 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 mt-4"
|
||||
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-sm 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" />
|
||||
<div className="animate-pulse h-8 w-64 bg-tertiary rounded-sm" />
|
||||
) : (
|
||||
<EmailInputSection
|
||||
email={email}
|
||||
|
||||
@@ -76,7 +76,7 @@ function VSCodeTab() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenInNewTab}
|
||||
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark transition-colors"
|
||||
className="px-4 py-2 bg-primary text-white rounded-sm hover:bg-primary-dark transition-colors"
|
||||
>
|
||||
{t("VSCODE$OPEN_IN_NEW_TAB")}
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin '../hero.ts';
|
||||
@config "../tailwind.config.js";
|
||||
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
|
||||
.button-base {
|
||||
@apply bg-tertiary border border-neutral-600 rounded;
|
||||
@apply bg-tertiary border border-neutral-600 rounded-xs;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
|
||||
1
frontend/src/types/conversation-status.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type ConversationStatus = "STARTING" | "RUNNING" | "STOPPED";
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @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;
|
||||
|
||||
|
||||
8
frontend/src/types/runtime-status.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
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,7 +5,10 @@
|
||||
* @returns The URL to redirect to for OAuth
|
||||
*/
|
||||
export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
|
||||
const redirectUri = `${requestUrl.origin}/oauth/keycloak/callback`;
|
||||
// Use HTTPS protocol unless the host is localhost
|
||||
const protocol =
|
||||
requestUrl.hostname === "localhost" ? requestUrl.protocol : "https:";
|
||||
const redirectUri = `${protocol}//${requestUrl.host}/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")
|
||||
|
||||
121
frontend/src/utils/status.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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,10 +2,6 @@
|
||||
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: {
|
||||
@@ -24,24 +20,5 @@ export default {
|
||||
},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [
|
||||
heroui({
|
||||
defaultTheme: "dark",
|
||||
layout: {
|
||||
radius: {
|
||||
small: "5px",
|
||||
large: "20px",
|
||||
},
|
||||
},
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
primary: "#4465DB",
|
||||
logo: "#CFB755",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
typography,
|
||||
],
|
||||
plugins: [typography],
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 {
|
||||
@@ -28,6 +29,7 @@ export default defineConfig(({ mode }) => {
|
||||
!process.env.VITEST && reactRouter(),
|
||||
viteTsconfigPaths(),
|
||||
svgr(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
port: FE_PORT,
|
||||
@@ -54,7 +56,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
ignored: ['**/node_modules/**', '**/.git/**'],
|
||||
ignored: ["**/node_modules/**", "**/.git/**"],
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
|
||||
54
microagents/code-review.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
triggers:
|
||||
- /codereview
|
||||
---
|
||||
|
||||
PERSONA:
|
||||
You are an expert software engineer and code reviewer with deep experience in modern programming best practices, secure coding, and clean code principles.
|
||||
|
||||
TASK:
|
||||
Review the code changes in this pull request or merge request, and provide actionable feedback to help the author improve code quality, maintainability, and security. DO NOT modify the code; only provide specific feedback.
|
||||
|
||||
CONTEXT:
|
||||
You have full context of the code being committed in the pull request or merge request, including the diff, surrounding files, and project structure. The code is written in a modern language and follows typical idioms and patterns for that language.
|
||||
|
||||
ROLE:
|
||||
As an automated reviewer, your role is to analyze the code changes and produce structured comments, including line numbers, across the following scenarios:
|
||||
|
||||
CODE REVIEW SCENARIOS:
|
||||
1. Style and Formatting
|
||||
Check for:
|
||||
- Inconsistent indentation, spacing, or bracket usage
|
||||
- Unused imports or variables
|
||||
- Non-standard naming conventions
|
||||
- Missing or misformatted comments/docstrings
|
||||
- Violations of common language-specific style guides (e.g., PEP8, Google Style Guide)
|
||||
|
||||
2. Clarity and Readability
|
||||
Identify:
|
||||
- Overly complex or deeply nested logic
|
||||
- Functions doing too much (violating single responsibility)
|
||||
- Poor naming that obscures intent
|
||||
- Missing inline documentation for non-obvious logic
|
||||
|
||||
3. Security and Common Bug Patterns
|
||||
Watch for:
|
||||
- Unsanitized user input (e.g., in SQL, shell, or web contexts)
|
||||
- Hardcoded secrets or credentials
|
||||
- Incorrect use of cryptographic libraries
|
||||
- Common pitfalls (null dereferencing, off-by-one errors, race conditions)
|
||||
|
||||
INSTRUCTIONS FOR RESPONSE:
|
||||
Group the feedback by the scenarios above.
|
||||
|
||||
Then, for each issue you find:
|
||||
- Provide a line number or line range
|
||||
- Briefly explain why it's an issue
|
||||
- Suggest a concrete improvement
|
||||
|
||||
Use the following structure in your output:
|
||||
[Line 42] :hammer_and_wrench: Unused import: The 'os' module is imported but never used. Remove it to clean up the code.
|
||||
[Lines 78–85] :mag: Readability: This nested if-else block is hard to follow. Consider refactoring into smaller functions or using early returns.
|
||||
[Line 102] :closed_lock_with_key: Security Risk: User input is directly concatenated into an SQL query. This could allow SQL injection. Use parameterized queries instead.
|
||||
|
||||
REMEMBER, DO NOT MODIFY THE CODE. ONLY PROVIDE FEEDBACK IN YOUR RESPONSE.
|
||||
@@ -14,6 +14,7 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it.
|
||||
* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename.
|
||||
* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times.
|
||||
* Temporary or utility files should go into /tmp instead of /workspace.
|
||||
</FILE_SYSTEM_GUIDELINES>
|
||||
|
||||
<CODE_QUALITY>
|
||||
|
||||
@@ -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 BrowseInteractiveAction(browser_actions='noop(1000)', return_axtree=True)
|
||||
|
||||
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 - use the first available provider from the list
|
||||
provider = provider_list[0] if provider_list else 'openai'
|
||||
# Set default provider - prefer 'anthropic' if available, otherwise use the first provider
|
||||
provider = 'anthropic' if 'anthropic' in provider_list else provider_list[0]
|
||||
model = None
|
||||
api_key = None
|
||||
|
||||
@@ -196,9 +196,11 @@ 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, use the first available provider
|
||||
# If the provider doesn't exist, prefer 'anthropic' if available, otherwise use the first provider
|
||||
provider = (
|
||||
next(iter(organized_models.keys())) if organized_models else 'openai'
|
||||
'anthropic'
|
||||
if 'anthropic' in organized_models
|
||||
else next(iter(organized_models.keys()))
|
||||
)
|
||||
|
||||
provider_models = organized_models[provider]['models']
|
||||
@@ -213,8 +215,10 @@ async def modify_llm_settings_basic(
|
||||
]
|
||||
provider_models = VERIFIED_ANTHROPIC_MODELS + provider_models
|
||||
|
||||
# Set default model to the first model in the list
|
||||
default_model = provider_models[0] if provider_models else 'gpt-4'
|
||||
# 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'
|
||||
)
|
||||
|
||||
# Show the default model but allow changing it
|
||||
print_formatted_text(
|
||||
|
||||
@@ -72,6 +72,7 @@ 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 = (
|
||||
@@ -1161,7 +1162,8 @@ class AgentController:
|
||||
|
||||
def _handle_long_context_error(self) -> None:
|
||||
# When context window is exceeded, keep roughly half of agent interactions
|
||||
kept_events = self._apply_conversation_window()
|
||||
current_view = View.from_events(self.state.history)
|
||||
kept_events = self._apply_conversation_window(current_view.events)
|
||||
kept_event_ids = {e.id for e in kept_events}
|
||||
|
||||
self.log(
|
||||
@@ -1198,7 +1200,7 @@ class AgentController:
|
||||
EventSource.AGENT,
|
||||
)
|
||||
|
||||
def _apply_conversation_window(self) -> list[Event]:
|
||||
def _apply_conversation_window(self, history: list[Event]) -> list[Event]:
|
||||
"""Cuts history roughly in half when context window is exceeded.
|
||||
|
||||
It preserves action-observation pairs and ensures that the system message,
|
||||
@@ -1217,11 +1219,9 @@ class AgentController:
|
||||
Returns:
|
||||
Filtered list of events keeping newest half while preserving pairs and essential initial events.
|
||||
"""
|
||||
if not self.state.history:
|
||||
# Handle empty history
|
||||
if not history:
|
||||
return []
|
||||
|
||||
history = self.state.history
|
||||
|
||||
# 1. Identify essential initial events
|
||||
system_message: SystemMessageAction | None = None
|
||||
first_user_msg: MessageAction | None = None
|
||||
@@ -1238,50 +1238,59 @@ class AgentController:
|
||||
and system_message.id == history[0].id
|
||||
)
|
||||
|
||||
# Find First User Message, which MUST exist
|
||||
first_user_msg = self._first_user_message()
|
||||
# Find First User Message in the history, which MUST exist
|
||||
first_user_msg = self._first_user_message(history)
|
||||
if first_user_msg is None:
|
||||
raise RuntimeError('No first user message found in the event stream.')
|
||||
# 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.',
|
||||
)
|
||||
|
||||
# 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
|
||||
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
|
||||
# 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)
|
||||
if first_user_msg:
|
||||
# Only include first user message if history is not empty
|
||||
if history:
|
||||
essential_events.append(first_user_msg)
|
||||
# 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)
|
||||
# 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)
|
||||
|
||||
# 2. Determine the slice of recent events to potentially keep
|
||||
num_non_essential_events = len(history) - len(essential_events)
|
||||
@@ -1430,15 +1439,32 @@ class AgentController:
|
||||
return result
|
||||
return False
|
||||
|
||||
def _first_user_message(self) -> MessageAction | None:
|
||||
def _first_user_message(
|
||||
self, events: list[Event] | None = None
|
||||
) -> 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')
|
||||
file_store_path: str = Field(default='~/.openhands')
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,7 @@ 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:
|
||||
@@ -33,6 +34,7 @@ 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,9 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from browsergym.utils.obs import flatten_axtree_to_str
|
||||
|
||||
from openhands.core.schema import ActionType, ObservationType
|
||||
from openhands.core.schema import ObservationType
|
||||
from openhands.events.observation.observation import Observation
|
||||
|
||||
|
||||
@@ -53,69 +51,5 @@ class BrowserOutputObservation(Observation):
|
||||
if self.screenshot_path:
|
||||
ret += f'Screenshot saved to: {self.screenshot_path}\n'
|
||||
ret += '--- Agent Observation ---\n'
|
||||
ret += self.get_agent_obs_text()
|
||||
ret += self.content
|
||||
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,7 +426,10 @@ def convert_tool_call_to_string(tool_call: dict) -> str:
|
||||
ret += f'<parameter={param_name}>'
|
||||
if is_multiline:
|
||||
ret += '\n'
|
||||
ret += f'{param_value}'
|
||||
if isinstance(param_value, list) or isinstance(param_value, dict):
|
||||
ret += json.dumps(param_value)
|
||||
else:
|
||||
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.get_agent_obs_text()
|
||||
text = obs.content
|
||||
if (
|
||||
obs.trigger_by_action == ActionType.BROWSE_INTERACTIVE
|
||||
and enable_som_visual_browsing
|
||||
|
||||
21
openhands/microagent/prompts/generate_remember_prompt.j2
Normal file
@@ -0,0 +1,21 @@
|
||||
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>
|
||||
@@ -59,6 +59,7 @@ 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 (
|
||||
@@ -67,16 +68,6 @@ 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 = {}
|
||||
@@ -124,6 +115,7 @@ 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__(
|
||||
@@ -187,6 +179,7 @@ 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:
|
||||
@@ -215,11 +208,12 @@ class Runtime(FileEditRuntimeMixin):
|
||||
message = f'[runtime {self.sid}] {message}'
|
||||
getattr(logger, level)(message, stacklevel=2)
|
||||
|
||||
def send_status_message(self, message_id: str):
|
||||
def set_runtime_status(self, runtime_status: RuntimeStatus):
|
||||
"""Sends a status message if the callback function was provided."""
|
||||
self.runtime_status = runtime_status
|
||||
if self.status_callback:
|
||||
msg = STATUS_MESSAGES.get(message_id, '')
|
||||
self.status_callback('info', message_id, msg)
|
||||
msg_id: str = runtime_status.value # type: ignore
|
||||
self.status_callback('info', msg_id, runtime_status.message)
|
||||
|
||||
def send_error_message(self, message_id: str, message: str):
|
||||
if self.status_callback:
|
||||
|
||||
@@ -2,7 +2,9 @@ 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
|
||||
@@ -14,6 +16,78 @@ 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,
|
||||
@@ -78,7 +152,8 @@ async def browse(
|
||||
image = png_base64_url_to_image(obs.get('screenshot'))
|
||||
image.save(screenshot_path, format='PNG', optimize=True)
|
||||
|
||||
return BrowserOutputObservation(
|
||||
# Create the observation with all data
|
||||
observation = 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
|
||||
@@ -103,13 +178,37 @@ 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:
|
||||
return BrowserOutputObservation(
|
||||
content=str(e),
|
||||
error_message = str(e)
|
||||
error_url = asked_url if action.action == ActionType.BROWSE else ''
|
||||
|
||||
# Create error observation
|
||||
observation = BrowserOutputObservation(
|
||||
content=error_message,
|
||||
screenshot='',
|
||||
screenshot_path=None,
|
||||
error=True,
|
||||
last_browser_action_error=str(e),
|
||||
url=asked_url if action.action == ActionType.BROWSE else '',
|
||||
last_browser_action_error=error_message,
|
||||
url=error_url,
|
||||
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,6 +49,7 @@ 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):
|
||||
@@ -126,7 +127,7 @@ class CLIRuntime(Runtime):
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Initialize the runtime connection."""
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
# Ensure workspace directory exists
|
||||
os.makedirs(self._workspace_path, exist_ok=True)
|
||||
@@ -138,7 +139,7 @@ class CLIRuntime(Runtime):
|
||||
await asyncio.to_thread(self.setup_initial_env)
|
||||
|
||||
self._runtime_initialized = True
|
||||
self.send_status_message('STATUS$CONTAINER_STARTED')
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
logger.info(f'CLIRuntime initialized with workspace at {self._workspace_path}')
|
||||
|
||||
def add_env_vars(self, env_vars: dict[str, Any]) -> None:
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
@@ -170,7 +171,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
super().check_if_alive()
|
||||
|
||||
async def connect(self):
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
should_start_action_execution_server = False
|
||||
|
||||
if self.attach_to_existing:
|
||||
@@ -179,7 +180,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
should_start_action_execution_server = True
|
||||
|
||||
if self.workspace is None:
|
||||
self.send_status_message('STATUS$PREPARING_CONTAINER')
|
||||
self.set_runtime_status(RuntimeStatus.BUILDING_RUNTIME)
|
||||
self.workspace = await call_sync_from_async(self._create_workspace)
|
||||
self.log('info', f'Created new workspace with id: {self.workspace_id}')
|
||||
|
||||
@@ -205,7 +206,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
self.log('info', 'Waiting for client to become ready...')
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
await call_sync_from_async(self._wait_until_alive)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
@@ -217,7 +218,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
self.send_status_message(' ')
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self._runtime_initialized = True
|
||||
|
||||
@tenacity.retry(
|
||||
|
||||
@@ -23,6 +23,7 @@ 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,
|
||||
@@ -145,7 +146,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
return self.api_url
|
||||
|
||||
async def connect(self) -> None:
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
try:
|
||||
await call_sync_from_async(self._attach_to_container)
|
||||
except docker.errors.NotFound as e:
|
||||
@@ -172,7 +173,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.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
await call_sync_from_async(self.wait_until_alive)
|
||||
|
||||
@@ -187,7 +188,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.send_status_message(' ')
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self._runtime_initialized = True
|
||||
|
||||
def maybe_build_runtime_container_image(self):
|
||||
@@ -196,7 +197,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
raise ValueError(
|
||||
'Neither runtime container image nor base container image is set'
|
||||
)
|
||||
self.send_status_message('STATUS$STARTING_CONTAINER')
|
||||
self.set_runtime_status(RuntimeStatus.BUILDING_RUNTIME)
|
||||
self.runtime_container_image = build_runtime_image(
|
||||
self.base_container_image,
|
||||
self.runtime_builder,
|
||||
@@ -267,7 +268,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
|
||||
def init_container(self) -> None:
|
||||
self.log('debug', 'Preparing to start container...')
|
||||
self.send_status_message('STATUS$PREPARING_CONTAINER')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
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
|
||||
@@ -376,7 +377,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
**(self.config.sandbox.docker_runtime_kwargs or {}),
|
||||
)
|
||||
self.log('debug', f'Container started. Server url: {self.api_url}')
|
||||
self.send_status_message('STATUS$CONTAINER_STARTED')
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
except Exception as e:
|
||||
self.log(
|
||||
'error',
|
||||
|
||||
@@ -35,6 +35,7 @@ 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
|
||||
@@ -206,7 +207,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.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
# Check if there's already a server running for this session ID
|
||||
if self.sid in _RUNNING_SERVERS:
|
||||
@@ -383,7 +384,7 @@ class LocalRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
self.log('info', f'Waiting for server to become ready at {self.api_url}...')
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
await call_sync_from_async(self._wait_until_alive)
|
||||
|
||||
@@ -395,7 +396,7 @@ class LocalRuntime(ActionExecutionClient):
|
||||
f'Server initialized with plugins: {[plugin.name for plugin in self.plugins]}',
|
||||
)
|
||||
if not self.attach_to_existing:
|
||||
self.send_status_message(' ')
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self._runtime_initialized = True
|
||||
|
||||
def _find_available_port(
|
||||
|
||||
@@ -13,6 +13,7 @@ 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,
|
||||
@@ -102,7 +103,7 @@ class ModalRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
async def connect(self):
|
||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
self.log('debug', f'ModalRuntime `{self.sid}`')
|
||||
|
||||
@@ -120,14 +121,14 @@ class ModalRuntime(ActionExecutionClient):
|
||||
sandbox_id, client=self.modal_client
|
||||
)
|
||||
else:
|
||||
self.send_status_message('STATUS$PREPARING_CONTAINER')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
await call_sync_from_async(
|
||||
self._init_sandbox,
|
||||
sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,
|
||||
plugins=self.plugins,
|
||||
)
|
||||
|
||||
self.send_status_message('STATUS$CONTAINER_STARTED')
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
|
||||
if self.sandbox is None:
|
||||
raise Exception('Sandbox not initialized')
|
||||
@@ -137,13 +138,13 @@ class ModalRuntime(ActionExecutionClient):
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.log('debug', 'Waiting for client to become ready...')
|
||||
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
self._wait_until_alive()
|
||||
self.setup_initial_env()
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.send_status_message(' ')
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self._runtime_initialized = True
|
||||
|
||||
@property
|
||||
|
||||