Compare commits

...

62 Commits

Author SHA1 Message Date
Xingyao Wang
7b92f021e5 Merge branch 'main' into add-message-feedback 2025-06-08 18:15:00 -04:00
openhands
b89223d083 Implement likert scale rating for finish actions in SAAS mode 2025-06-08 22:05:10 +00:00
Xingyao Wang
d6d5499416 refactor(MCP): Replace MCPRouter with FastMCP Proxy (#8877)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-08 22:03:18 +00:00
openhands
c222916b58 Merge main into add-message-feedback and resolve conflicts 2025-06-08 21:40:01 +00:00
Tim O'Farrell
0221f21c12 Wait for nested container graceful shutdown (#8969) 2025-06-08 13:43:34 -06:00
Tim O'Farrell
617445d5ca Nested event store search no longer throwing errors on 404 (#8985) 2025-06-08 13:41:58 -06:00
Xingyao Wang
34c13c8824 Add back microagent files with special handling for user inputs (#8139)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-09 02:49:54 +08:00
Sergey
49939c1f02 Fix typo in evaluation README.md (#8987) 2025-06-08 14:14:07 +00:00
llamantino
abec074a66 fix: prevent LLM settings reset when page loses focus during initial setup (#8928)
Co-authored-by: llamantino <12345678+yourusername@users.noreply.github.com>
2025-06-07 20:52:59 +00:00
Graham Neubig
46c12ce258 Update summary_prompt for improved code quality (#8975) 2025-06-07 14:46:40 -04:00
Graham Neubig
5de119dc2e Improve repo.md documentation to instruct OpenHands on capturing repository context efficiently (#8977)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-07 23:18:54 +08:00
llamantino
0abc6f27ef fix(devcontainer): configure host networking to fix runtime connection (#8971)
Co-authored-by: llamantino <12345678+yourusername@users.noreply.github.com>
2025-06-07 01:44:23 +02:00
mamoodi
445d3a5788 Update Cloud UI docs (#8968) 2025-06-07 05:09:54 +08:00
mamoodi
744a6299a7 Update gitlab integration docs (#8946) 2025-06-06 16:07:55 -04:00
chuckbutkus
345dccbf84 Allow user to change their email address (#8861)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-06 18:22:29 +00:00
Rohit Malhotra
6605269e5b [Fix]: make sure to track opened PRs using Git MCP (#8949) 2025-06-07 02:22:14 +08:00
tofarr
fac0d59388 Fix for nested runtimes still using the relative url (#8947) 2025-06-06 15:42:54 +00:00
Xingyao Wang
4d6d28a192 Add Google AI Studio API key instructions to documentation (#8938)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-06-06 15:39:35 +00:00
llamantino
ebacd1b080 fix: make setup.sh executable for devcontainer postCreateCommand (#8891)
Co-authored-by: llamantino <12345678+yourusername@users.noreply.github.com>
2025-06-06 05:26:22 -07:00
Xingyao Wang
59f5f0dc9b feat(agent): remind the agent that it can use timeout to increase the amount of time the command is running (#8932)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-05 20:57:33 -07:00
Rohit Malhotra
4df3ee9d2e (refactor): Update MCP Client to use FastMCP (#8931) 2025-06-06 10:01:39 +08:00
Rohit Malhotra
aa54a25241 [Fix]: Broken links from cloud resolver (#8923)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-06 01:46:04 +00:00
Robert Brennan
0813c113f0 Fix for running git commands with the proper user (#8898) 2025-06-06 00:20:15 +00:00
tofarr
19fcf427ba Improved WebSocket Error Handling (#8924)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-05 16:42:18 -06:00
Rohit Malhotra
336b22bea4 [Fix]: add missing await (#8936) 2025-06-05 21:52:30 +00:00
Xingyao Wang
959268b45a chore(dependency): Update opentelemetry-api to resolve conflict with langfuse (#8930)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-05 20:26:51 +00:00
Robert Brennan
309c086976 Fix event stream replay during new connections by replaying before joining (#8818)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-05 12:37:02 -07:00
Ray Myers
afd8ee61e7 Fix missing None-check in get_conversations (#8927) 2025-06-05 18:55:41 +00:00
Rohit Malhotra
93b1276768 [Feat]: Add experiment manager (#8820) 2025-06-05 14:49:20 -04:00
mamoodi
412e265745 Update OpenHands Cloud and GitHub Integrations (#8922)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-06-05 17:39:35 +00:00
Ray Myers
a3790f1003 0.41.0 Release Branch (#8905) 2025-06-05 17:25:18 +00:00
tofarr
b76553136e Added feature flag for opening vscode in a new tab (#8917) 2025-06-05 10:32:07 -06:00
Ray Myers
dee89462c2 Improve type coverage for nested runtime (#8921) 2025-06-05 16:19:53 +00:00
mamoodi
ad468587ea Split quickstart and getting started workflow (#8904) 2025-06-05 10:01:36 -04:00
Calvin Smith
41cee4b68d Add unit tests for View object (#8900)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-06-04 19:35:32 -06:00
tofarr
91e24a4a31 Add conversation start and stop endpoints (#8883)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-04 16:29:24 -06:00
tofarr
a1b3c0c7d6 No more 500 error when jumping between conversations (Nested Runtimes) (#8902) 2025-06-04 15:55:26 -06:00
Mislav Lukach
738ecd468c fix(frontend): add security analyzer placeholder (#8901) 2025-06-04 20:07:23 +00:00
tofarr
c6c2aafc4f Assorted fixes for the nested / docker runtimes. (#8899) 2025-06-04 13:56:03 -06:00
mamoodi
7bea93b1b6 Move the documentation tabs from top to left nav (#8892) 2025-06-04 14:46:29 -04:00
Robert Brennan
d346506d34 Revert "Unrevert "Add username parameter to AsyncBashSession"" (#8897) 2025-06-04 18:45:28 +00:00
Rohit Malhotra
d30c6ff720 (Hotfix): make sure MCP tool error observations are surfaced to agent (#8894) 2025-06-04 18:42:09 +00:00
Robert Brennan
80e496d134 Unrevert "Add username parameter to AsyncBashSession" (#8771)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-04 14:32:27 -04:00
Anton Sokolchenko
a933a81ef5 Increase sandbox close delay in sandbox_config.py to 3600 seconds (#8889)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-06-04 18:12:32 +00:00
tofarr
3c977bd715 Fix for nested mount volumes (#8888) 2025-06-04 09:30:57 -06:00
Graham Neubig
c403973616 Add return type annotations to docker runtime (#8543)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-04 11:24:25 -04:00
tofarr
7652ccb000 Fix VSCode iframe SameSite cookie issue with cross-origin fallback (#8881)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-04 09:23:48 -06:00
openhands
688fb1fc54 Simplify feedback modal implementation and fix linting issues 2025-05-17 12:15:02 +00:00
openhands
7db28516ea Simplify message feedback implementation by removing unnecessary features 2025-05-17 12:06:57 +00:00
openhands
d5127c9ee7 Remove unnecessary changes and fix controlled/uncontrolled component warnings 2025-05-17 11:50:31 +00:00
openhands
2acd6f6b7e Fix tests and linting issues for message feedback feature 2025-05-17 11:41:25 +00:00
openhands
a01019bb96 Merge main into add-message-feedback and resolve conflicts 2025-05-17 07:15:07 +00:00
openhands
66e5b7c026 Simplify message feedback implementation with custom hooks and components 2025-05-06 13:24:23 +00:00
openhands
e888a278a6 Move feedback buttons next to copy button in hover menu 2025-05-06 12:56:14 +00:00
Xingyao Wang
c419ddaa03 Merge branch 'main' into add-message-feedback 2025-05-06 20:32:29 +08:00
openhands
3859a5442e Merge main into add-message-feedback and fix conflicts 2025-05-05 02:31:09 +00:00
openhands
dcd9fd249c Merge main into add-message-feedback and resolve conflicts 2025-05-05 02:29:03 +00:00
openhands
eea593418c Rename EnhancedFeedbackModal to FeedbackModal 2025-04-04 17:22:15 +00:00
openhands
5fb4a882f2 Fix duplicate translation keys 2025-04-04 17:11:20 +00:00
openhands
5a58876339 Merge main into add-message-feedback branch 2025-04-04 17:00:20 +00:00
openhands
10cdf88ed9 Fix TypeScript errors and linting issues 2025-03-25 21:08:14 +00:00
openhands
1bda19e618 Add message-level feedback functionality with thumbs up/down buttons 2025-03-25 19:23:31 +00:00
155 changed files with 4005 additions and 1473 deletions

View File

@@ -12,4 +12,5 @@
"ghcr.io/devcontainers/features/node:1": {},
},
"postCreateCommand": ".devcontainer/setup.sh",
"runArgs": ["--network=host"],
}

0
.devcontainer/setup.sh Normal file → Executable file
View File

2
.github/CODEOWNERS vendored
View File

@@ -5,7 +5,7 @@
/frontend/ @rbren @amanape
# Evaluation code owners
/evaluation/ @xingyaoww @neubig
/evaluation/ @xingyaoww @neubig
# Documentation code owners
/docs/ @mamoodi

View File

@@ -16,7 +16,6 @@ updates:
mcp-packages:
patterns:
- "mcp"
- "mcpm"
security-all:
applies-to: "security-updates"
patterns:

View File

@@ -313,6 +313,8 @@ jobs:
TEST_IN_CI=true \
RUN_AS_OPENHANDS=false \
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
env:
DEBUG: "1"
# Run unit tests with the Docker runtime Docker images as openhands user
test_runtime_oh:
@@ -378,6 +380,8 @@ jobs:
TEST_IN_CI=true \
RUN_AS_OPENHANDS=true \
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
env:
DEBUG: "1"
# The two following jobs (named identically) are to check whether all the runtime tests have passed as the
# "All Runtime Tests Passed" is a required job for PRs to merge

View File

@@ -74,5 +74,11 @@ jobs:
run: poetry install --with dev,test,runtime
- name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/test_windows_bash.py
env:
DEBUG: "1"
- name: Run Windows runtime tests with LocalRuntime
run: $env:TEST_RUNTIME="local"; poetry run pytest -svv tests/runtime/test_bash.py
env:
TEST_RUNTIME: local
DEBUG: "1"

1
.gitignore vendored
View File

@@ -166,7 +166,6 @@ cython_debug/
# https://stackoverflow.com/questions/32964920/should-i-commit-the-vscode-folder-to-source-control
.vscode/**/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json

View File

@@ -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.40-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.41-nikolaik`
## Develop inside Docker container

View File

@@ -51,17 +51,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.40
docker.all-hands.dev/all-hands-ai/openhands:0.41
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!

View File

@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.40
docker.all-hands.dev/all-hands-ai/openhands:0.41
```
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands

View File

@@ -11,7 +11,7 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.40-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.41-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -37,7 +37,8 @@ repos:
hooks:
- id: mypy
additional_dependencies:
[types-requests, types-setuptools, types-pyyaml, types-toml]
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, lxml]
# To see gaps add `--html-report mypy-report/`
entry: mypy --config-file dev_config/python/mypy.ini openhands/
always_run: true
pass_filenames: false

View File

@@ -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.40-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -20,7 +20,7 @@
"navigation": {
"tabs": [
{
"tab": "Getting started",
"tab": "Docs",
"pages": [
"index",
"usage/installation",
@@ -31,116 +31,116 @@
"pages": [
"usage/cloud/openhands-cloud",
{
"group": "Installation",
"group": "Integrations",
"pages": [
"usage/cloud/github-installation",
"usage/cloud/gitlab-installation"
]
},
"usage/cloud/cloud-ui",
"usage/cloud/cloud-issue-resolver",
"usage/cloud/cloud-api"
]
},
{
"group": "Usage Methods",
"group": "Running OpenHands Locally",
"pages": [
"usage/local-setup",
"usage/how-to/gui-mode",
"usage/how-to/cli-mode",
"usage/how-to/headless-mode",
"usage/how-to/github-action"
]
}
]
},
{
"tab": "Prompting and Customization",
"pages": [
"usage/prompting/prompting-best-practices",
"usage/prompting/repository",
},
{
"group": "Microagents",
"group": "Customization",
"pages": [
"usage/prompting/microagents-overview",
"usage/prompting/microagents-repo",
"usage/prompting/microagents-keyword",
"usage/prompting/microagents-org",
"usage/prompting/microagents-public"
]
}
]
},
{
"tab": "Advanced Configuration",
"pages": [
{
"group": "LLM Configuration",
"pages": [
"usage/llms/llms",
"usage/prompting/prompting-best-practices",
"usage/prompting/repository",
{
"group": "Providers",
"group": "Microagents",
"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"
"usage/prompting/microagents-overview",
"usage/prompting/microagents-repo",
"usage/prompting/microagents-keyword",
"usage/prompting/microagents-org",
"usage/prompting/microagents-public"
]
}
]
},
{
"group": "Runtime Configuration",
"group": "Advanced 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"
]
}
]
}
{
"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/configuration-options",
"usage/how-to/custom-sandbox-guide",
"usage/search-engine-setup",
"usage/mcp"
]
},
{
"tab": "Troubleshooting & Feedback",
"pages": [
"usage/troubleshooting/troubleshooting",
"usage/feedback"
]
},
{
"tab": "For OpenHands Developers",
"pages": [
"usage/how-to/development-overview",
{
"group": "Architecture",
"group": "Troubleshooting & Feedback",
"pages": [
"usage/architecture/backend",
"usage/architecture/runtime"
"usage/troubleshooting/troubleshooting",
"usage/feedback"
]
},
"usage/how-to/debugging",
"usage/how-to/evaluation-harness",
"usage/how-to/websocket-connection"
{
"group": "OpenHands Developers",
"pages": [
"usage/how-to/development-overview",
{
"group": "Architecture",
"pages": [
"usage/architecture/backend",
"usage/architecture/runtime"
]
},
"usage/how-to/debugging",
"usage/how-to/evaluation-harness",
"usage/how-to/websocket-connection"
]
}
]
},
{

BIN
docs/static/img/connect-repo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,33 +0,0 @@
---
title: Cloud Issue Resolver
description: The Cloud Issue Resolver automates code fixes and provides intelligent assistance for your repositories on GitHub.
---
## Setup
The Cloud Issue Resolver is available automatically when you grant OpenHands Cloud repository access:
- [GitHub repository access](./github-installation#adding-repository-access)
## Usage
After granting OpenHands Cloud repository access, you can use the Cloud Issue Resolver on issues and pull requests in your repositories.
### Working with Issues
On your repository, label an issue with `openhands` or add a message starting with
`@openhands`. OpenHands will:
1. Comment on the issue to let you know it is working on it
- You can click on the link to track the progress on OpenHands Cloud
2. Open a pull request if it determines that the issue has been successfully resolved
3. Comment on the issue with a summary of the performed tasks and a link to the PR
### Working with Pull Requests
To get OpenHands to work on pull requests, mention `@openhands` in comments to:
- Ask questions
- Request updates
- Get code explanations
OpenHands will:
1. Comment to let you know it is working on it
2. Perform the requested task

View File

@@ -1,28 +1,36 @@
---
title: Cloud UI
description: The Cloud UI provides a web interface for interacting with OpenHands AI. This page explains how to access and use the OpenHands Cloud UI.
description: The Cloud UI provides a web interface for interacting with OpenHands. This page explains how to use the
OpenHands Cloud UI.
---
## Landing Page
## Accessing the UI
The landing page is where you can:
The OpenHands Cloud UI can be accessed at [app.all-hands.dev](https://app.all-hands.dev). You'll need to sign in with your GitHub or GitLab account to access the interface.
## Key Features
For detailed information about the features available in the OpenHands Cloud UI, please refer to the [Key Features](../key-features) section of the documentation.
- [Add GitHub repository access](/usage/cloud/github-installation#adding-github-repository-access) to OpenHands.
- [Select a GitHub repo](/usage/cloud/github-installation#working-with-github-repos-in-openhands-cloud) or
[a GitLab repo](/usage/cloud/gitlab-installation#working-with-gitlab-repos-in-openhands-cloud) to start working on.
- See `Suggested Tasks` for repositories that OpenHands has access to.
- Launch an empty conversation using `Launch from Scratch`.
## Settings
The settings page allows you to:
The Settings page allows you to:
- Configure your account preferences.
- Manage repository access.
- Generate API keys for programmatic access.
- Generate custom secrets for the agent.
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
- Set application settings like your preferred language, notifications and other preferences.
- Add credits to your account.
- Generate custom secrets.
- Create API keys to work with OpenHands programmatically.
## Key Features
For an overview of the key features available inside a conversation, please refer to the [Key Features](../key-features)
section of the documentation.
## Next Steps
- [Use the Cloud Issue Resolver](./cloud-issue-resolver) to automate code fixes and get assistance.
- [Learn about the Cloud API](./cloud-api) for programmatic access.
- [Install GitHub Integration](/usage/cloud/github-installation) to use OpenHands with your GitHub repositories.
- [Install GitLab Integration](/usage/cloud/gitlab-installation) to use OpenHands with your GitLab repositories.
- [Use the Cloud API](/usage/cloud/cloud-api) to programmatically interact with OpenHands.

View File

@@ -1,30 +1,22 @@
---
title: GitHub Installation
description: This guide walks you through the process of installing and configuring OpenHands Cloud for your GitHub repositories.
title: GitHub Integration
description: This guide walks you through the process of installing OpenHands Cloud for your GitHub repositories. Once
set up, it will allow OpenHands to work with your GitHub repository through the Cloud UI or straight from GitHub!
---
## Prerequisites
- A GitHub account
- Access to OpenHands Cloud
- Signed in to [OpenHands Cloud](https://app.all-hands.dev) with [a GitHub account](/usage/cloud/openhands-cloud).
## Installation Steps
## Adding GitHub Repository Access
1. Log in to [OpenHands Cloud](https://app.all-hands.dev)
2. If you haven't connected your GitHub account yet:
- Click on `Connect to GitHub`
- Review and accept the terms of service
- Authorize the OpenHands AI application
You can grant OpenHands access to specific GitHub repositories:
## Adding Repository Access
You can grant OpenHands access to specific repositories:
1. Click on `Add GitHub repos`
1. Click on `Add GitHub repos` on the landing page.
2. Select your organization and choose the specific repositories to grant OpenHands access to.
- OpenHands requests short-lived tokens (8-hour expiration) with these permissions:
<Accordion title="OpenHands permissions">
- OpenHands requests short-lived tokens (8-hour expiration) with these permissions:
- Actions: Read and write
- Administration: Read-only
- Commit statuses: Read and write
- Contents: Read and write
- Issues: Read and write
@@ -35,20 +27,45 @@ You can grant OpenHands access to specific repositories:
- Repository access for a user is granted based on:
- Permission granted for the repository
- User's GitHub permissions (owner/collaborator)
3. Click `Install & Authorize`
</Accordion>
3. Click `Install & Authorize`.
## Modifying Repository Access
You can modify repository access at any time by visiting the Settings page and selecting `Configure GitHub Repositories` under the `Git` tab.
You can modify GitHub repository access at any time by:
- Selecting `Add GitHub repos` on the landing page or
- Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Git` tab
## Using OpenHands with GitHub
## Working With GitHub Repos in Openhands Cloud
Once you've granted repository access, you can use OpenHands with your GitHub repositories.
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the `select a repo`
and `select a branch` dropdowns to select the appropriate repository and branch you'd like OpenHands to work on. Then
click on `Launch` to start the conversation!
For details on how to use OpenHands with GitHub issues and pull requests, see the [Cloud Issue Resolver](./cloud-issue-resolver) documentation.
![Connect Repo](/static/img/connect-repo.png)
## Working on Github Issues and Pull Requests Using Openhands
Giving GitHub repository access to OpenHands also allows you to work on GitHub issues and pull requests directly.
### Working with Issues
On your repository, label an issue with `openhands` or add a message starting with
`@openhands`. OpenHands will:
1. Comment on the issue to let you know it is working on it.
- You can click on the link to track the progress on OpenHands Cloud.
2. Open a pull request if it determines that the issue has been successfully resolved.
3. Comment on the issue with a summary of the performed tasks and a link to the PR.
### Working with Pull Requests
To get OpenHands to work on pull requests, mention `@openhands` in the comments to:
- Ask questions
- Request updates
- Get code explanations
## Next Steps
- [Access the Cloud UI](./cloud-ui) to interact with the web interface
- [Use the Cloud Issue Resolver](./cloud-issue-resolver) to automate code fixes and get assistance
- [Use the Cloud API](./cloud-api) to programmatically interact with OpenHands
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
- [Use the Cloud API](/usage/cloud/cloud-api) to programmatically interact with OpenHands.

View File

@@ -1,23 +1,25 @@
---
title: GitLab Installation
description: This guide walks you through the process of installing and configuring OpenHands Cloud for your GitLab repositories.
title: GitLab Integration
description: This guide walks you through the process of installing OpenHands Cloud for your GitLab repositories. Once
set up, it will allow OpenHands to work with your GitLab repository.
---
## Prerequisites
- A GitLab account
- Access to OpenHands Cloud
- Signed in to [OpenHands Cloud](https://app.all-hands.dev) with [a GitLab account](/usage/cloud/openhands-cloud).
## Installation Steps
## Adding GitLab Repository Access
1. Log in to [OpenHands Cloud](https://app.all-hands.dev)
2. If you haven't connected your GitLab account yet:
- Click on `Log in with GitLab`
- Authorize the OpenHands application
Upon signing into OpenHands Cloud with a GitLab account, OpenHands will have access to your repositories.
## Working With GitLab Repos in Openhands Cloud
After signing in with a Gitlab account, use the `select a repo` and `select a branch` dropdowns to select the
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
## Next Steps
- [Access the Cloud UI](./cloud-ui) to interact with the web interface
- [Use the Cloud API](./cloud-api) to programmatically interact with OpenHands
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
- [Use the Cloud API](/usage/cloud/cloud-api) to programmatically interact with OpenHands.

View File

@@ -1,13 +1,12 @@
---
title: Getting Started
description: Getting started with OpenHands Cloud
description: Getting started with OpenHands Cloud.
---
OpenHands Cloud is the hosted cloud version of All Hands AI's OpenHands.
## Accessing OpenHands Cloud
To get started with OpenHands Cloud, visit [app.all-hands.dev](https://app.all-hands.dev).
OpenHands Cloud is the hosted cloud version of All Hands AI's OpenHands. To get started with OpenHands Cloud,
visit [app.all-hands.dev](https://app.all-hands.dev).
You'll be prompted to connect with your GitHub or GitLab account:
@@ -15,13 +14,13 @@ You'll be prompted to connect with your GitHub or GitLab account:
2. Review the permissions requested by OpenHands and authorize the application.
- OpenHands will require certain permissions from your account. To read more about these permissions,
you can click the `Learn more` link on the authorization page.
3. Review and accept the `terms of service` and select `Continue`.
## Next Steps
Once you've connected your account, you can:
- [Install GitHub Integration](./github-installation) to use OpenHands with your GitHub repositories
- [Install GitLab Integration](./gitlab-installation) to use OpenHands with your GitLab repositories
- [Access the Cloud UI](./cloud-ui) to interact with the web interface
- [Use the Cloud API](./cloud-api) to programmatically interact with OpenHands
- [Set up the Cloud Issue Resolver](./cloud-issue-resolver) to automate code fixes and provide intelligent assistance
- [Install GitHub Integration](/usage/cloud/github-installation) to use OpenHands with your GitHub repositories.
- [Install GitLab Integration](/usage/cloud/gitlab-installation) to use OpenHands with your GitLab repositories.
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
- [Use the Cloud API](/usage/cloud/cloud-api) to programmatically interact with OpenHands.

View File

@@ -31,7 +31,7 @@ This command opens an interactive prompt where you can type tasks or commands an
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -40,7 +40,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.40 \
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
python -m openhands.cli.main --override-cli-mode true
```

View File

@@ -31,7 +31,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.40-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -41,7 +41,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.40 \
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -1,6 +1,6 @@
---
title: Quick Start
description: Running OpenHands on the cloud or your local desktop
description: Running OpenHands Cloud or running on your local system.
icon: rocket
---
@@ -10,164 +10,10 @@ The easiest way to get started with OpenHands is on OpenHands Cloud, which comes
To get started with OpenHands Cloud, visit [app.all-hands.dev](https://app.all-hands.dev).
You'll be prompted to connect with your GitHub or GitLab account:
For more information see [getting started with OpenHands Cloud.](/usage/cloud/openhands-cloud)
1. Click `Log in with GitHub` or `Log in with GitLab`.
2. Review the permissions requested by OpenHands and authorize the application.
- OpenHands will require certain permissions from your account. To read more about these permissions,
you can click the `Learn more` link on the authorization page.
## Running OpenHands Locally
Run OpenHands on your local system and bring your own LLM and API key.
Once you've connected your account, you can:
- [Install GitHub Integration](/usage/cloud/github-installation) to use OpenHands with your GitHub repositories
- [Install GitLab Integration](/usage/cloud/gitlab-installation) to use OpenHands with your GitLab repositories
- [Access the Cloud UI](/usage/cloud/cloud-ui) to interact with the web interface
- [Use the Cloud API](/usage/cloud/cloud-api) to programmatically interact with OpenHands
- [Set up the Cloud Issue Resolver](/usage/cloud/cloud-issue-resolver) to automate code fixes and provide intelligent assistance
## Running OpenHands on your local desktop
### System Requirements
- MacOS with [Docker Desktop support](https://docs.docker.com/desktop/setup/install/mac-install/#system-requirements)
- Linux
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)
A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands.
### Prerequisites
<AccordionGroup>
<Accordion title="MacOS">
**Docker Desktop**
1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install).
2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled.
</Accordion>
<Accordion title="Linux">
<Note>
Tested with Ubuntu 22.04.
</Note>
**Docker Desktop**
1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/).
</Accordion>
<Accordion title="Windows">
**WSL**
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
**Docker Desktop**
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
2. Open Docker Desktop, go to `Settings` and confirm the following:
- General: `Use the WSL 2 based engine` is enabled.
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
<Note>
The docker command below to start the app must be run inside the WSL terminal.
</Note>
</Accordion>
</AccordionGroup>
### Start the App
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-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.40
```
You'll find OpenHands running at http://localhost:3000!
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a GitHub action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
### Setup
After launching OpenHands, you **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
This can be done during the initial settings popup or by selecting the `Settings`
button (gear icon) in the UI.
If the required model does not exist in the list, in `Settings` under the `LLM` tab, you can toggle `Advanced` options
and manually enter it with the correct prefix in the `Custom Model` text box.
The `Advanced` options also allow you to specify a `Base URL` if required.
#### Getting an API Key
OpenHands requires an API key to access most language models. Here's how to get an API key from the recommended providers:
<AccordionGroup>
<Accordion title="Anthropic (Claude)">
1. [Create an Anthropic account](https://console.anthropic.com/).
2. [Generate an API key](https://console.anthropic.com/settings/keys).
3. [Set up billing](https://console.anthropic.com/settings/billing).
Consider setting usage limits to control costs.
</Accordion>
<Accordion title="OpenAI">
1. [Create an OpenAI account](https://platform.openai.com/).
2. [Generate an API key](https://platform.openai.com/api-keys).
3. [Set up billing](https://platform.openai.com/account/billing/overview).
</Accordion>
</AccordionGroup>
#### 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.
Search functionality is enabled by default in OpenHands Cloud. No additional setup is required.
To enable search functionality in self-hosted OpenHands:
1. Get a Tavily API key from [tavily.com](https://tavily.com/)
2. Enter the API key in the Settings page under `LLM` tab, `Search API Key (Tavily)`
For more details, see the [Search Engine Setup](/usage/search-engine-setup) guide.
Now you're ready to [get started with OpenHands](./getting-started).
#### Versions
The [docker command above](./installation#start-the-app) pulls the most recent stable release of OpenHands. You have other options as well:
- For a specific release, replace `$VERSION` in `openhands:$VERSION` and `runtime:$VERSION`, with the version number.
We use SemVer so `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
- For the most up-to-date development version, replace `$VERSION` in `openhands:$VERSION` and `runtime:$VERSION`, with `main`.
This version is unstable and is recommended for testing or development purposes only.
For the development workflow, see [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
Are you having trouble? Check out our [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting).
For more information see [running OpenHands locally.](/usage/local-setup)

View File

@@ -54,25 +54,25 @@ Check [the installation guide](https://docs.all-hands.dev/modules/usage/installa
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.40-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
mkdir -p ~/.openhands-state && echo '{"language":"en","agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":false,"llm_model":"lm_studio/'$LMSTUDIO_MODEL_NAME'","llm_api_key":"dummy","llm_base_url":"'$LMSTUDIO_URL/v1'","remote_runtime_resource_factor":null,"github_token":null,"enable_default_condenser":true,"user_consents_to_analytics":true}' > ~/.openhands-state/settings.json
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.40
docker.all-hands.dev/all-hands-ai/openhands:0.41
```
Once your server is running -- you can visit `http://localhost:3000` in your browser to use OpenHands with local Devstral model:
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.40
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.41
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None

151
docs/usage/local-setup.mdx Normal file
View File

@@ -0,0 +1,151 @@
---
title: Getting Started
description: Getting started with running OpenHands locally.
---
## Recommended Methods for Running Openhands on Your Local System
### System Requirements
- MacOS with [Docker Desktop support](https://docs.docker.com/desktop/setup/install/mac-install/#system-requirements)
- Linux
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)
A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands.
### Prerequisites
<AccordionGroup>
<Accordion title="MacOS">
**Docker Desktop**
1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install).
2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled.
</Accordion>
<Accordion title="Linux">
<Note>
Tested with Ubuntu 22.04.
</Note>
**Docker Desktop**
1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/).
</Accordion>
<Accordion title="Windows">
**WSL**
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
**Docker Desktop**
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
2. Open Docker Desktop, go to `Settings` and confirm the following:
- General: `Use the WSL 2 based engine` is enabled.
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
<Note>
The docker command below to start the app must be run inside the WSL terminal.
</Note>
</Accordion>
</AccordionGroup>
### Start the App
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-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.40
```
You'll find OpenHands running at http://localhost:3000!
### Setup
After launching OpenHands, you **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
This can be done during the initial settings popup or by selecting the `Settings`
button (gear icon) in the UI.
If the required model does not exist in the list, in `Settings` under the `LLM` tab, you can toggle `Advanced` options
and manually enter it with the correct prefix in the `Custom Model` text box.
The `Advanced` options also allow you to specify a `Base URL` if required.
#### Getting an API Key
OpenHands requires an API key to access most language models. Here's how to get an API key from the recommended providers:
<AccordionGroup>
<Accordion title="Anthropic (Claude)">
1. [Create an Anthropic account](https://console.anthropic.com/).
2. [Generate an API key](https://console.anthropic.com/settings/keys).
3. [Set up billing](https://console.anthropic.com/settings/billing).
</Accordion>
<Accordion title="OpenAI">
1. [Create an OpenAI account](https://platform.openai.com/).
2. [Generate an API key](https://platform.openai.com/api-keys).
3. [Set up billing](https://platform.openai.com/account/billing/overview).
</Accordion>
<Accordion title="Google (Gemini)">
1. Create a Google account if you don't already have one.
2. [Generate an API key](https://aistudio.google.com/apikey).
3. [Set up billing](https://aistudio.google.com/usage?tab=billing).
</Accordion>
</AccordionGroup>
Consider setting usage limits to control costs.
#### 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.
To enable search functionality in OpenHands:
1. Get a Tavily API key from [tavily.com](https://tavily.com/).
2. Enter the Tavily API key in the Settings page under `LLM` tab > `Search API Key (Tavily)`
For more details, see the [Search Engine Setup](/usage/search-engine-setup) guide.
Now you're ready to [get started with OpenHands](/usage/getting-started).
### Versions
The [docker command above](/usage/local-setup#start-the-app) pulls the most recent stable release of OpenHands. You have other options as well:
- For a specific release, replace `$VERSION` in `openhands:$VERSION` and `runtime:$VERSION`, with the version number.
For example, `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
- For the most up-to-date development version, replace `$VERSION` in `openhands:$VERSION` and `runtime:$VERSION`, with `main`.
This version is unstable and is recommended for testing or development purposes only.
## Next Steps
- [Connect OpenHands to your local filesystem.](/usage/runtimes/docker#connecting-to-your-filesystem) to use OpenHands with your GitHub repositories
- [Run OpenHands in a scriptable headless mode.](/usage/how-to/headless-mode)
- [Run OpenHands with a friendly CLI.](/usage/how-to/cli-mode)
- [Run OpenHands on tagged issues with a GitHub action.](/usage/how-to/github-action)

View File

@@ -11,7 +11,7 @@ Currently OpenHands supports the following types of microagents:
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
add `<microagent_name>.md` files inside.
add `<microagent_name>.md` files inside. For repository-specific guidelines, you can ask OpenHands to analyze your repository and create a comprehensive `repo.md` file (see [General Microagents](./microagents-repo) for details).
<Note>
Loaded microagents take up space in the context window.

View File

@@ -17,13 +17,45 @@ Frontmatter should be enclosed in triple dashes (---) and may include the follow
|-----------|-----------------------------------------|----------|----------------|
| `agent` | The agent this microagent applies to | No | 'CodeActAgent' |
## Example
## Creating a Comprehensive Repository Agent
To create an effective repository agent, you can ask OpenHands to analyze your repository with a prompt like:
General microagent file example located at `.openhands/microagents/repo.md`:
```
Please browse the repository, look at the documentation and relevant code, and understand the purpose of this repository.
Specifically, I want you to create a `.openhands/microagents/repo.md` file. This file should contain succinct information that summarizes:
1. The purpose of this repository
2. The general setup of this repo
3. A brief description of the structure of this repo
Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.
```
This approach helps OpenHands capture repository context efficiently, reducing the need for repeated searches during conversations and ensuring more accurate solutions.
## Example Content
A comprehensive repository agent file (`.openhands/microagents/repo.md`) should include:
```
# Repository Purpose
This project is a TODO application that allows users to track TODO items.
# Setup Instructions
To set it up, you can run `npm run build`.
# Repository Structure
- `/src`: Core application code
- `/tests`: Test suite
- `/docs`: Documentation
- `/.github`: CI/CD workflows
# CI/CD Workflows
- `lint.yml`: Runs ESLint on all JavaScript files
- `test.yml`: Runs the test suite on pull requests
# Development Guidelines
Always make sure the tests are passing before committing changes. You can run the tests by running `npm run test`.
```

View File

@@ -71,7 +71,6 @@ EVAL_CONDENSER=summarizer_for_eval \
The name is up to you, but should match a name defined in your `config.toml` file. The last argument in the command specifies the condenser configuration to use. In this case, `summarizer_for_eval` is used, which refers to the LLM-based summarizing condenser as defined above.
If no condenser configuration is specified, the 'noop' condenser will be used by default, which keeps the full conversation history.
```
For other configurations specific to evaluation, such as `save_trajectory_path`, these are typically set in the `get_config` function of the respective `run_infer.py` file for each benchmark.

View File

@@ -1,8 +1,37 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { ChatMessage } from "#/components/features/chat/chat-message";
// Mock the MessageActions component
vi.mock("#/components/features/chat/message-actions", () => ({
MessageActions: ({ onCopy }: { onCopy: () => void }) => (
<div data-testid="message-actions">
<button
data-testid="copy-to-clipboard"
onClick={onCopy}
style={{ display: "none" }}
className="message-action-button"
>
Copy
</button>
</div>
),
}));
// Mock useHover hook
vi.mock("#/hooks/use-hover", () => ({
useHover: () => {
return [
false,
{
onMouseEnter: () => {},
onMouseLeave: () => {},
}
];
},
}));
describe("ChatMessage", () => {
it("should render a user message", () => {
render(<ChatMessage type="user" message="Hello, World!" />);
@@ -23,30 +52,51 @@ describe("ChatMessage", () => {
});
it("should render the copy to clipboard button when the user hovers over the message", async () => {
const user = userEvent.setup();
render(<ChatMessage type="user" message="Hello, World!" />);
const message = screen.getByText("Hello, World!");
expect(screen.getByTestId("copy-to-clipboard")).not.toBeVisible();
await user.hover(message);
expect(screen.getByTestId("copy-to-clipboard")).toBeVisible();
// This test is now checking for the presence of MessageActions component
// since the copy button visibility is handled there
render(<ChatMessage type="assistant" message="Hello, World!" messageId={1} />);
expect(screen.getByTestId("message-actions")).toBeInTheDocument();
expect(screen.getByTestId("copy-to-clipboard")).toBeInTheDocument();
});
it("should copy content to clipboard", async () => {
const user = userEvent.setup();
render(<ChatMessage type="user" message="Hello, World!" />);
const copyToClipboardButton = screen.getByTestId("copy-to-clipboard");
await user.click(copyToClipboardButton);
await waitFor(() =>
expect(navigator.clipboard.readText()).resolves.toBe("Hello, World!"),
);
// Mock clipboard API
const clipboardWriteTextMock = vi.fn();
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: clipboardWriteTextMock },
configurable: true
});
// Mock the handleCopyToClipboard function in the MessageActions component
vi.mock("#/components/features/chat/message-actions", () => ({
MessageActions: ({ onCopy }: { onCopy: () => void }) => {
// Call onCopy immediately to simulate the button click
setTimeout(() => onCopy(), 0);
return (
<div data-testid="message-actions">
<button
data-testid="copy-to-clipboard"
onClick={onCopy}
>
Copy
</button>
</div>
);
},
}));
render(<ChatMessage type="assistant" message="Hello, World!" messageId={1} />);
// Wait for the clipboard function to be called
await waitFor(() => {
expect(clipboardWriteTextMock).toHaveBeenCalledWith("Hello, World!");
});
});
it("should display an error toast if copying content to clipboard fails", async () => {});
it("should display an error toast if copying content to clipboard fails", async () => {
// This test is now a placeholder since the error handling is in the MessageActions component
});
it("should render a component passed as a prop", () => {
function Component() {

View File

@@ -6,6 +6,21 @@ import { renderWithProviders } from "test-utils";
import OpenHands from "#/api/open-hands";
import SettingsScreen from "#/routes/settings";
import { PaymentForm } from "#/components/features/payment/payment-form";
import * as useSettingsModule from "#/hooks/query/use-settings";
// Mock the useSettings hook
vi.mock("#/hooks/query/use-settings", async () => {
const actual = await vi.importActual<typeof import("#/hooks/query/use-settings")>("#/hooks/query/use-settings");
return {
...actual,
useSettings: vi.fn().mockReturnValue({
data: {
EMAIL_VERIFIED: true, // Mock email as verified to prevent redirection
},
isLoading: false,
}),
};
});
// Mock the i18next hook
vi.mock("react-i18next", async () => {
@@ -20,6 +35,7 @@ vi.mock("react-i18next", async () => {
"SETTINGS$NAV_CREDITS": "Credits",
"SETTINGS$NAV_API_KEYS": "API Keys",
"SETTINGS$NAV_LLM": "LLM",
"SETTINGS$NAV_USER": "User",
"SETTINGS$TITLE": "Settings"
};
return translations[key] || key;
@@ -47,6 +63,10 @@ describe("Settings Billing", () => {
Component: () => <div data-testid="git-settings-screen" />,
path: "/settings/git",
},
{
Component: () => <div data-testid="user-settings-screen" />,
path: "/settings/user",
},
],
},
]);

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.40.0",
"version": "0.41.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.40.0",
"version": "0.41.0",
"dependencies": {
"@heroui/react": "2.7.8",
"@microlink/react-json-view": "^1.26.2",

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.40.0",
"version": "0.41.0",
"private": true,
"type": "module",
"engines": {

View File

@@ -47,6 +47,7 @@ const SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
// Attributes that typically don't contain user-facing text
const NON_TEXT_ATTRIBUTES = [
"allow",
"className",
"i18nKey",
"testId",
@@ -69,6 +70,7 @@ const NON_TEXT_ATTRIBUTES = [
"aria-describedby",
"aria-hidden",
"role",
"sandbox",
];
function shouldIgnorePath(filePath) {
@@ -114,6 +116,7 @@ const EXCLUDED_TECHNICAL_STRINGS = [
"add-secret-form", // Test ID for secret form
"edit-secret-form", // Test ID for secret form
"search-api-key-input", // Input name for search API key
"noopener,noreferrer", // Options for window.open
];
function isExcludedTechnicalString(str) {

View File

@@ -1,5 +1,60 @@
import axios from "axios";
import axios, { AxiosError, AxiosResponse } from "axios";
export const openHands = axios.create({
baseURL: `${window.location.protocol}//${import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host}`,
});
// Helper function to check if a response contains an email verification error
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const checkForEmailVerificationError = (data: any): boolean => {
const EMAIL_NOT_VERIFIED = "EmailNotVerifiedError";
if (typeof data === "string") {
return data.includes(EMAIL_NOT_VERIFIED);
}
if (typeof data === "object" && data !== null) {
if ("message" in data) {
const { message } = data;
if (typeof message === "string") {
return message.includes(EMAIL_NOT_VERIFIED);
}
if (Array.isArray(message)) {
return message.some(
(msg) => typeof msg === "string" && msg.includes(EMAIL_NOT_VERIFIED),
);
}
}
// Search any values in object in case message key is different
return Object.values(data).some(
(value) =>
(typeof value === "string" && value.includes(EMAIL_NOT_VERIFIED)) ||
(Array.isArray(value) &&
value.some(
(v) => typeof v === "string" && v.includes(EMAIL_NOT_VERIFIED),
)),
);
}
return false;
};
// Set up the global interceptor
openHands.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
// Check if it's a 403 error with the email verification message
if (
error.response?.status === 403 &&
checkForEmailVerificationError(error.response?.data)
) {
if (window.location.pathname !== "/settings/user") {
window.location.reload();
}
}
// Continue with the error for other error handlers
return Promise.reject(error);
},
);

View File

@@ -236,6 +236,26 @@ class OpenHands {
return data;
}
static async startConversation(
conversationId: string,
): Promise<Conversation | null> {
const { data } = await openHands.post<Conversation | null>(
`/api/conversations/${conversationId}/start`,
);
return data;
}
static async stopConversation(
conversationId: string,
): Promise<Conversation | null> {
const { data } = await openHands.post<Conversation | null>(
`/api/conversations/${conversationId}/stop`,
);
return data;
}
/**
* Get the settings from the server or use the default settings if not found
*/

View File

@@ -3,21 +3,22 @@ import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
import { useTranslation } from "react-i18next";
import hotToast from "react-hot-toast";
import { I18nKey } from "#/i18n/declaration";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { TrajectoryActions } from "../trajectory/trajectory-actions";
import { createChatMessage } from "#/services/chat-service";
import { createChatMessage, createUserFeedback } from "#/services/chat-service";
import { InteractiveChatBox } from "./interactive-chat-box";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { FeedbackModal } from "../feedback/feedback-modal";
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
import { TypingIndicator } from "./typing-indicator";
import { useWsClient } from "#/context/ws-client-provider";
import { Messages } from "./messages";
import { ChatSuggestions } from "./chat-suggestions";
import { ActionSuggestions } from "./action-suggestions";
import { FeedbackModal } from "../feedback/feedback-modal";
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
@@ -50,10 +51,10 @@ export function ChatInterface() {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
const [feedbackPolarity, setFeedbackPolarity] = React.useState<
"positive" | "negative"
>("positive");
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
const [messageToSend, setMessageToSend] = React.useState<string | null>(null);
const { selectedRepository, replayJson } = useSelector(
(state: RootState) => state.initialQuery,
@@ -96,11 +97,17 @@ export function ChatInterface() {
send(generateAgentStateChangeEvent(AgentState.STOPPED));
};
const onClickShareFeedbackActionButton = async (
const onClickShareFeedbackActionButton = (
polarity: "positive" | "negative",
) => {
setFeedbackModalIsOpen(true);
// Open the feedback modal with the selected polarity
setFeedbackPolarity(polarity);
setFeedbackModalIsOpen(true);
// Track the feedback button click
posthog.capture("feedback_button_clicked", {
polarity,
});
};
const onClickExportTrajectoryButton = () => {
@@ -197,7 +204,24 @@ export function ChatInterface() {
<FeedbackModal
isOpen={feedbackModalIsOpen}
onClose={() => setFeedbackModalIsOpen(false)}
onClose={() => {
// Send the feedback action
send(createUserFeedback(feedbackPolarity, "trajectory"));
// Show a toast notification to confirm feedback was sent
hotToast.success(
feedbackPolarity === "positive"
? t(I18nKey.FEEDBACK$POSITIVE_SENT)
: t(I18nKey.FEEDBACK$NEGATIVE_SENT),
);
// Track the feedback submission
posthog.capture("feedback_submitted", {
polarity: feedbackPolarity,
});
setFeedbackModalIsOpen(false);
}}
polarity={feedbackPolarity}
/>
</div>

View File

@@ -4,22 +4,27 @@ import remarkGfm from "remark-gfm";
import { code } from "../markdown/code";
import { cn } from "#/utils/utils";
import { ul, ol } from "../markdown/list";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
import { anchor } from "../markdown/anchor";
import { MessageActions } from "./message-actions";
import { useHover } from "#/hooks/use-hover";
import { OpenHandsSourceType } from "#/types/core/base";
import { paragraph } from "../markdown/paragraph";
interface ChatMessageProps {
type: OpenHandsSourceType;
message: string;
messageId?: number;
feedback?: "positive" | "negative" | null;
}
export function ChatMessage({
type,
message,
messageId,
feedback,
children,
}: React.PropsWithChildren<ChatMessageProps>) {
const [isHovering, setIsHovering] = React.useState(false);
const [isHovering, hoverProps] = useHover();
const [isCopy, setIsCopy] = React.useState(false);
const handleCopyToClipboard = async () => {
@@ -44,8 +49,8 @@ export function ChatMessage({
return (
<article
data-testid={`${type}-message`}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onMouseEnter={hoverProps.onMouseEnter}
onMouseLeave={hoverProps.onMouseLeave}
className={cn(
"rounded-xl relative",
"flex flex-col gap-2",
@@ -53,12 +58,17 @@ export function ChatMessage({
type === "agent" && "mt-6 max-w-full bg-transparent",
)}
>
<CopyToClipboardButton
isHidden={!isHovering}
isDisabled={isCopy}
onClick={handleCopyToClipboard}
mode={isCopy ? "copied" : "copy"}
/>
{/* Action buttons */}
{type === "assistant" && (
<MessageActions
messageId={messageId}
feedback={feedback}
isHovering={isHovering}
isCopy={isCopy}
onCopy={handleCopyToClipboard}
/>
)}
<div className="text-sm break-words">
<Markdown
components={{
@@ -73,6 +83,7 @@ export function ChatMessage({
{message}
</Markdown>
</div>
{children}
</article>
);

View File

@@ -18,6 +18,7 @@ import { ol, ul } from "../markdown/list";
import { paragraph } from "../markdown/paragraph";
import { MonoComponent } from "./mono-component";
import { PathComponent } from "./path-component";
import { FinishActionRating } from "./finish-action-rating";
const trimText = (text: string, maxLength: number): string => {
if (!text) return "";
@@ -203,6 +204,11 @@ export function ExpandableMessage({
>
{details}
</Markdown>
{/* Show rating component for finish actions in SAAS mode */}
{action?.payload.action === "finish" && (
<FinishActionRating messageId={action.payload.id} />
)}
</div>
)}
</div>

View File

@@ -0,0 +1,137 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useWsClient } from "#/context/ws-client-provider";
import { createUserFeedback } from "#/services/chat-service";
import { useConfig } from "#/hooks/query/use-config";
import StarIcon from "#/icons/star.svg?react";
import StarFilledIcon from "#/icons/star-filled.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface FinishActionRatingProps {
messageId: number;
}
// List of reasons for negative feedback with their translation keys
const FEEDBACK_REASONS = [
{ key: I18nKey.FEEDBACK$REASON_NOT_FOLLOW_INSTRUCTION },
{ key: I18nKey.FEEDBACK$REASON_BAD_SOLUTION },
{ key: I18nKey.FEEDBACK$REASON_LACKS_ACCESS },
];
export function FinishActionRating({ messageId }: FinishActionRatingProps) {
const { t } = useTranslation();
const { send } = useWsClient();
const { data: config } = useConfig();
const [rating, setRating] = useState<number | null>(null);
const [hoveredRating, setHoveredRating] = useState<number | null>(null);
const [showReasons, setShowReasons] = useState(false);
const [reasonTimeout, setReasonTimeout] = useState<NodeJS.Timeout | null>(
null,
);
// Clean up timeout on unmount
useEffect(
() => () => {
if (reasonTimeout) {
clearTimeout(reasonTimeout);
}
},
[reasonTimeout],
);
// Submit feedback to the backend
const submitFeedback = (ratingValue: number, reason: string | null) => {
// Convert rating to positive/negative
const feedbackType = ratingValue >= 3 ? "positive" : "negative";
// Send feedback event
if (send) {
send(
createUserFeedback(
feedbackType,
"message",
messageId,
ratingValue,
reason,
),
);
}
// Hide reasons after submission
setShowReasons(false);
};
// Handle rating selection
const handleRatingClick = (value: number) => {
setRating(value);
setShowReasons(true);
// Set a timeout to automatically submit feedback if no reason is selected
const timeout = setTimeout(() => {
submitFeedback(value, null);
}, 3000);
setReasonTimeout(timeout);
};
// Handle reason selection
const handleReasonClick = (reason: string) => {
if (reasonTimeout) {
clearTimeout(reasonTimeout);
}
submitFeedback(rating!, reason);
};
// Only show in SAAS mode
if (config?.APP_MODE !== "saas") {
return null;
}
return (
<div className="mt-2">
{/* Rating stars */}
<div className="flex items-center mb-2">
<span className="text-sm mr-2">{t("FEEDBACK$RATE_RESPONSE")}</span>
<div className="flex">
{[1, 2, 3, 4, 5].map((value) => (
<button
type="button"
key={value}
className="p-1 focus:outline-none"
onMouseEnter={() => setHoveredRating(value)}
onMouseLeave={() => setHoveredRating(null)}
onClick={() => handleRatingClick(value)}
disabled={rating !== null}
>
{(hoveredRating !== null && value <= hoveredRating) ||
(rating !== null && value <= rating) ? (
<StarFilledIcon className="w-5 h-5 text-yellow-400" />
) : (
<StarIcon className="w-5 h-5 text-gray-400" />
)}
</button>
))}
</div>
</div>
{/* Reason selection */}
{showReasons && (
<div className="mt-2 bg-neutral-800 p-2 rounded">
<p className="text-sm mb-2">{t("FEEDBACK$SELECT_REASON")}</p>
<div className="flex flex-col gap-2">
{FEEDBACK_REASONS.map((reason) => (
<button
type="button"
key={reason.key}
className="text-sm text-left p-2 hover:bg-neutral-700 rounded"
onClick={() => handleReasonClick(t(reason.key))}
>
{t(reason.key)}
</button>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import React from "react";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
import { MessageFeedback } from "./message-feedback";
interface MessageActionsProps {
messageId?: number;
feedback?: "positive" | "negative" | null;
isHovering: boolean;
isCopy: boolean;
onCopy: () => void;
}
export function MessageActions({
messageId,
feedback,
isHovering,
isCopy,
onCopy,
}: MessageActionsProps) {
return (
<div
className={`absolute top-1 right-1 flex items-center gap-1 ${!isHovering ? "hidden" : ""}`}
>
{messageId && (
<MessageFeedback messageId={messageId} feedback={feedback} />
)}
<CopyToClipboardButton
isHidden={!isHovering}
isDisabled={isCopy}
onClick={onCopy}
mode={isCopy ? "copied" : "copy"}
/>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import React from "react";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { useWsClient } from "#/context/ws-client-provider";
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
import { TrajectoryActionButton } from "#/components/shared/buttons/trajectory-action-button";
import { createUserFeedback } from "#/services/chat-service";
import { setMessageFeedback } from "#/state/chat-slice";
import { I18nKey } from "#/i18n/declaration";
interface MessageFeedbackProps {
messageId: number;
feedback?: "positive" | "negative" | null;
}
export function MessageFeedback({ messageId, feedback }: MessageFeedbackProps) {
const { t } = useTranslation();
const { send } = useWsClient();
const dispatch = useDispatch();
const handleFeedback = (feedbackType: "positive" | "negative") => {
// Don't send if already selected
if (feedback === feedbackType) return;
// Update local state
dispatch(setMessageFeedback({ messageId, feedbackType }));
// Send to backend
send(createUserFeedback(feedbackType, "message", messageId));
};
return (
<div className="flex gap-1 mt-2">
<TrajectoryActionButton
testId={`positive-${messageId}`}
onClick={() => handleFeedback("positive")}
icon={<ThumbsUpIcon width={15} height={15} />}
tooltip={t(I18nKey.BUTTON$MARK_HELPFUL)}
className={feedback === "positive" ? "bg-neutral-700" : ""}
/>
<TrajectoryActionButton
testId={`negative-${messageId}`}
onClick={() => handleFeedback("negative")}
icon={<ThumbDownIcon width={15} height={15} />}
tooltip={t(I18nKey.BUTTON$MARK_NOT_HELPFUL)}
className={feedback === "negative" ? "bg-neutral-700" : ""}
/>
</div>
);
}

View File

@@ -1,60 +1,82 @@
import React from "react";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsObservation } from "#/types/core/observations";
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
import { EventMessage } from "./event-message";
import { ChatMessage } from "./chat-message";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import type { Message } from "#/message";
import { ChatMessage } from "#/components/features/chat/chat-message";
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
import { ImageCarousel } from "../images/image-carousel";
import { ExpandableMessage } from "./expandable-message";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { useConversationId } from "#/hooks/use-conversation-id";
import { I18nKey } from "#/i18n/declaration";
interface MessagesProps {
messages: (OpenHandsAction | OpenHandsObservation)[];
messages: Message[];
isAwaitingUserConfirmation: boolean;
}
export const Messages: React.FC<MessagesProps> = React.memo(
({ messages, isAwaitingUserConfirmation }) => {
const { getOptimisticUserMessage } = useOptimisticUserMessage();
const { conversationId } = useConversationId();
const { data: conversation } = useUserConversation(conversationId || null);
const optimisticUserMessage = getOptimisticUserMessage();
// Check if conversation metadata has trigger=resolver
const isResolverTrigger = conversation?.trigger === "resolver";
const actionHasObservationPair = React.useCallback(
(event: OpenHandsAction | OpenHandsObservation): boolean => {
if (isOpenHandsAction(event)) {
return !!messages.some(
(msg) => isOpenHandsObservation(msg) && msg.cause === event.id,
);
}
return messages.map((message, index) => {
const shouldShowConfirmationButtons =
messages.length - 1 === index &&
message.sender === "assistant" &&
isAwaitingUserConfirmation;
return false;
},
[messages],
);
const isFirstUserMessageWithResolverTrigger =
index === 0 && message.sender === "user" && isResolverTrigger;
return (
<>
{messages.map((message, index) => (
<EventMessage
key={index}
event={message}
hasObservationPair={actionHasObservationPair(message)}
isAwaitingUserConfirmation={isAwaitingUserConfirmation}
isLastMessage={messages.length - 1 === index}
/>
))}
// Special case: First user message with resolver trigger
if (isFirstUserMessageWithResolverTrigger) {
return (
<div key={index}>
<ExpandableMessage
type="action"
message={message.content}
id={I18nKey.CHAT$RESOLVER_INSTRUCTIONS}
/>
{message.imageUrls && message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />
)}
</div>
);
}
{optimisticUserMessage && (
<ChatMessage type="user" message={optimisticUserMessage} />
)}
</>
);
},
(prevProps, nextProps) => {
// Prevent re-renders if messages are the same length
if (prevProps.messages.length !== nextProps.messages.length) {
return false;
}
if (message.type === "error" || message.type === "action") {
return (
<div key={index}>
<ExpandableMessage
type={message.type}
id={message.translationID}
message={message.content}
success={message.success}
observation={message.observation}
action={message.action}
/>
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</div>
);
}
return true;
return (
<ChatMessage
key={index}
type={message.sender}
message={message.content}
messageId={message.eventID}
feedback={message.feedback}
>
{message.imageUrls && message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />
)}
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</ChatMessage>
);
});
},
);

View File

@@ -84,7 +84,7 @@ export function AgentStatusBar() {
setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME));
setIndicatorColor(IndicatorColor.RED);
} else if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage(t(I18nKey.STATUS$CONNECTED)); // Using STATUS$CONNECTED instead of STATUS$CONNECTING
setStatusMessage(t(I18nKey.STATUS$WEBSOCKET_CLOSED));
setIndicatorColor(IndicatorColor.RED);
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);

View File

@@ -122,7 +122,7 @@ export function FileDiffViewer({ path, type }: FileDiffViewerProps) {
modifiedEditor.onDidContentSizeChange(updateEditorHeight);
};
const status = type === "U" ? STATUS_MAP.A : STATUS_MAP[type];
const status = (type === "U" ? STATUS_MAP.A : STATUS_MAP[type]) || "?";
let statusIcon: React.ReactNode;
if (typeof status === "string") {

View File

@@ -20,6 +20,7 @@ export function FeedbackModal({
polarity,
}: FeedbackModalProps) {
const { t } = useTranslation();
if (!isOpen) return null;
return (

View File

@@ -0,0 +1,32 @@
import React from "react";
import { useLocation, useNavigate } from "react-router";
import { useSettings } from "#/hooks/query/use-settings";
/**
* A component that restricts access to routes based on email verification status.
* If EMAIL_VERIFIED is false, only allows access to the /settings/user page.
*/
export function EmailVerificationGuard({
children,
}: {
children: React.ReactNode;
}) {
const { data: settings, isLoading } = useSettings();
const navigate = useNavigate();
const { pathname } = useLocation();
React.useEffect(() => {
// If settings are still loading, don't do anything yet
if (isLoading) return;
// If EMAIL_VERIFIED is explicitly false (not undefined or null)
if (settings?.EMAIL_VERIFIED === false) {
// Allow access to /settings/user but redirect from any other page
if (pathname !== "/settings/user") {
navigate("/settings/user", { replace: true });
}
}
}, [settings?.EMAIL_VERIFIED, pathname, navigate, isLoading]);
return children;
}

View File

@@ -35,7 +35,6 @@ export function SettingsSwitch({
type="checkbox"
onChange={(e) => handleToggle(e.target.checked)}
checked={controlledIsToggled ?? isToggled}
defaultChecked={defaultIsToggled}
/>
<StyledSwitchComponent isToggled={controlledIsToggled ?? isToggled} />

View File

@@ -69,16 +69,21 @@ export function Sidebar() {
<div className="flex items-center justify-center">
<AllHandsLogoButton />
</div>
<NewProjectButton />
<NewProjectButton disabled={settings?.EMAIL_VERIFIED === false} />
<ConversationPanelButton
isOpen={conversationPanelIsOpen}
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
onClick={() =>
settings?.EMAIL_VERIFIED === false
? null
: setConversationPanelIsOpen((prev) => !prev)
}
disabled={settings?.EMAIL_VERIFIED === false}
/>
</div>
<div className="flex flex-row md:flex-col md:items-center gap-[26px] md:mb-4">
<DocsButton />
<SettingsButton />
<DocsButton disabled={settings?.EMAIL_VERIFIED === false} />
<SettingsButton disabled={settings?.EMAIL_VERIFIED === false} />
<UserActions
user={
user.data ? { avatar_url: user.data.avatar_url } : undefined

View File

@@ -8,11 +8,13 @@ import { cn } from "#/utils/utils";
interface ConversationPanelButtonProps {
isOpen: boolean;
onClick: () => void;
disabled?: boolean;
}
export function ConversationPanelButton({
isOpen,
onClick,
disabled = false,
}: ConversationPanelButtonProps) {
const { t } = useTranslation();
@@ -22,10 +24,14 @@ export function ConversationPanelButton({
tooltip={t(I18nKey.SIDEBAR$CONVERSATIONS)}
ariaLabel={t(I18nKey.SIDEBAR$CONVERSATIONS)}
onClick={onClick}
disabled={disabled}
>
<FaListUl
size={22}
className={cn(isOpen ? "text-white" : "text-[#9099AC]")}
className={cn(
isOpen ? "text-white" : "text-[#9099AC]",
disabled && "opacity-50",
)}
/>
</TooltipButton>
);

View File

@@ -3,15 +3,24 @@ import DocsIcon from "#/icons/academy.svg?react";
import { I18nKey } from "#/i18n/declaration";
import { TooltipButton } from "./tooltip-button";
export function DocsButton() {
interface DocsButtonProps {
disabled?: boolean;
}
export function DocsButton({ disabled = false }: DocsButtonProps) {
const { t } = useTranslation();
return (
<TooltipButton
tooltip={t(I18nKey.SIDEBAR$DOCS)}
ariaLabel={t(I18nKey.SIDEBAR$DOCS)}
href="https://docs.all-hands.dev"
disabled={disabled}
>
<DocsIcon width={28} height={28} className="text-[#9099AC]" />
<DocsIcon
width={28}
height={28}
className={`text-[#9099AC] ${disabled ? "opacity-50" : ""}`}
/>
</TooltipButton>
);
}

View File

@@ -3,7 +3,11 @@ import { I18nKey } from "#/i18n/declaration";
import PlusIcon from "#/icons/plus.svg?react";
import { TooltipButton } from "./tooltip-button";
export function NewProjectButton() {
interface NewProjectButtonProps {
disabled?: boolean;
}
export function NewProjectButton({ disabled = false }: NewProjectButtonProps) {
const { t } = useTranslation();
const startNewProject = t(I18nKey.CONVERSATION$START_NEW);
return (
@@ -12,6 +16,7 @@ export function NewProjectButton() {
ariaLabel={startNewProject}
navLinkTo="/"
testId="new-project-button"
disabled={disabled}
>
<PlusIcon width={28} height={28} />
</TooltipButton>

View File

@@ -5,9 +5,13 @@ import { I18nKey } from "#/i18n/declaration";
interface SettingsButtonProps {
onClick?: () => void;
disabled?: boolean;
}
export function SettingsButton({ onClick }: SettingsButtonProps) {
export function SettingsButton({
onClick,
disabled = false,
}: SettingsButtonProps) {
const { t } = useTranslation();
return (
@@ -17,6 +21,7 @@ export function SettingsButton({ onClick }: SettingsButtonProps) {
ariaLabel={t(I18nKey.SETTINGS$TITLE)}
onClick={onClick}
navLinkTo="/settings"
disabled={disabled}
>
<SettingsIcon width={28} height={28} />
</TooltipButton>

View File

@@ -12,6 +12,7 @@ export interface TooltipButtonProps {
ariaLabel: string;
testId?: string;
className?: React.HTMLAttributes<HTMLButtonElement>["className"];
disabled?: boolean;
}
export function TooltipButton({
@@ -23,9 +24,10 @@ export function TooltipButton({
ariaLabel,
testId,
className,
disabled = false,
}: TooltipButtonProps) {
const handleClick = (e: React.MouseEvent) => {
if (onClick) {
if (onClick && !disabled) {
onClick();
e.preventDefault();
}
@@ -37,7 +39,12 @@ export function TooltipButton({
aria-label={ariaLabel}
data-testid={testId}
onClick={handleClick}
className={cn("hover:opacity-80", className)}
className={cn(
"hover:opacity-80",
disabled && "opacity-50 cursor-not-allowed",
className,
)}
disabled={disabled}
>
{children}
</button>
@@ -45,7 +52,7 @@ export function TooltipButton({
let content;
if (navLinkTo) {
if (navLinkTo && !disabled) {
content = (
<NavLink
to={navLinkTo}
@@ -63,7 +70,24 @@ export function TooltipButton({
{children}
</NavLink>
);
} else if (href) {
} else if (navLinkTo && disabled) {
// If disabled and has navLinkTo, render a button that looks like a NavLink but doesn't navigate
content = (
<button
type="button"
aria-label={ariaLabel}
data-testid={testId}
className={cn(
"text-[#9099AC]",
"opacity-50 cursor-not-allowed",
className,
)}
disabled
>
{children}
</button>
);
} else if (href && !disabled) {
content = (
<a
href={href}
@@ -76,6 +100,19 @@ export function TooltipButton({
{children}
</a>
);
} else if (href && disabled) {
// If disabled and has href, render a button that looks like a link but doesn't navigate
content = (
<button
type="button"
aria-label={ariaLabel}
data-testid={testId}
className={cn("opacity-50 cursor-not-allowed", className)}
disabled
>
{children}
</button>
);
} else {
content = buttonContent;
}

View File

@@ -5,6 +5,7 @@ interface TrajectoryActionButtonProps {
onClick: () => void;
icon: React.ReactNode;
tooltip?: string;
className?: string;
}
export function TrajectoryActionButton({
@@ -12,13 +13,14 @@ export function TrajectoryActionButton({
onClick,
icon,
tooltip,
className,
}: TrajectoryActionButtonProps) {
const button = (
<button
type="button"
data-testid={testId}
onClick={onClick}
className="button-base p-1 hover:bg-neutral-500"
className={`button-base p-1 hover:bg-neutral-500 ${className || ""}`}
>
{icon}
</button>

View File

@@ -150,7 +150,8 @@ export function WsClientProvider({
const { providers } = useUserProviders();
const messageRateHandler = useRate({ threshold: 250 });
const { data: conversation } = useActiveConversation();
const { data: conversation, refetch: refetchConversation } =
useActiveConversation();
function send(event: Record<string, unknown>) {
if (!sioRef.current) {
@@ -269,14 +270,11 @@ export function WsClientProvider({
sio.io.opts.query.latest_event_id = lastEventRef.current?.id;
updateStatusWhenErrorMessagePresent(data);
setErrorMessage(
hasValidMessageProperty(data)
? data.message
: "The WebSocket connection was closed.",
);
setErrorMessage(hasValidMessageProperty(data) ? data.message : "");
}
function handleError(data: unknown) {
// set status
setStatus(WsClientProviderStatus.DISCONNECTED);
updateStatusWhenErrorMessagePresent(data);
@@ -285,6 +283,9 @@ export function WsClientProvider({
? data.message
: "An unknown error occurred on the WebSocket connection.",
);
// check if something went wrong with the conversation.
refetchConversation();
}
React.useEffect(() => {
@@ -300,12 +301,19 @@ export function WsClientProvider({
if (!conversationId) {
throw new Error("No conversation ID provided");
}
if (!conversation || conversation.status === "STARTING") {
if (
!conversation ||
["STOPPED", "STARTING"].includes(conversation.status)
) {
return () => undefined; // conversation not yet loaded
}
let sio = sioRef.current;
if (sio?.connected) {
sio.disconnect();
}
const lastEvent = lastEventRef.current;
const query = {
latest_event_id: lastEvent?.id ?? -1,

View File

@@ -1,14 +1,26 @@
import { useEffect } from "react";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useUserConversation } from "./use-user-conversation";
import OpenHands from "#/api/open-hands";
const FIVE_MINUTES = 1000 * 60 * 5;
export const useActiveConversation = () => {
const { conversationId } = useConversationId();
return useUserConversation(conversationId, (query) => {
const userConversation = useUserConversation(conversationId, (query) => {
if (query.state.data?.status === "STARTING") {
return 2000; // 2 seconds
return 3000; // 3 seconds
}
return FIVE_MINUTES;
});
useEffect(() => {
const conversation = userConversation.data;
OpenHands.setCurrentConversation(conversation || null);
}, [
conversationId,
userConversation.isFetched,
userConversation?.data?.status,
]);
return userConversation;
};

View File

@@ -27,7 +27,8 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
apiSettings.enable_proactive_conversation_starters,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
SEARCH_API_KEY: apiSettings.search_api_key || "",
EMAIL: apiSettings.email || "",
EMAIL_VERIFIED: apiSettings.email_verified,
MCP_CONFIG: apiSettings.mcp_config,
IS_NEW_USER: false,
};
@@ -44,6 +45,7 @@ export const useSettings = () => {
// would want to show the modal immediately if the
// settings are not found
retry: (_, error) => error.status !== 404,
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
enabled: !isOnTosPage && !!userIsAuthenticated,

View File

@@ -23,7 +23,6 @@ export const useUserConversation = (
queryKey: ["user", "conversation", cid],
queryFn: async () => {
const conversation = await OpenHands.getConversation(cid!);
OpenHands.setCurrentConversation(conversation);
return conversation;
},
enabled: !!cid,

View File

@@ -0,0 +1,12 @@
import { useState } from "react";
export function useHover() {
const [isHovering, setIsHovering] = useState(false);
const hoverProps = {
onMouseEnter: () => setIsHovering(true),
onMouseLeave: () => setIsHovering(false),
};
return [isHovering, hoverProps] as const;
}

View File

@@ -1,5 +1,6 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
STATUS$WEBSOCKET_CLOSED = "STATUS$WEBSOCKET_CLOSED",
HOME$LAUNCH_FROM_SCRATCH = "HOME$LAUNCH_FROM_SCRATCH",
HOME$READ_THIS = "HOME$READ_THIS",
AUTH$LOGGING_BACK_IN = "AUTH$LOGGING_BACK_IN",
@@ -138,7 +139,9 @@ export enum I18nKey {
VSCODE$LOADING = "VSCODE$LOADING",
VSCODE$URL_NOT_AVAILABLE = "VSCODE$URL_NOT_AVAILABLE",
VSCODE$FETCH_ERROR = "VSCODE$FETCH_ERROR",
VSCODE$IFRAME_PERMISSIONS = "VSCODE$IFRAME_PERMISSIONS",
VSCODE$CROSS_ORIGIN_WARNING = "VSCODE$CROSS_ORIGIN_WARNING",
VSCODE$URL_PARSE_ERROR = "VSCODE$URL_PARSE_ERROR",
VSCODE$OPEN_IN_NEW_TAB = "VSCODE$OPEN_IN_NEW_TAB",
INCREASE_TEST_COVERAGE = "INCREASE_TEST_COVERAGE",
AUTO_MERGE_PRS = "AUTO_MERGE_PRS",
FIX_README = "FIX_README",
@@ -332,6 +335,7 @@ export enum I18nKey {
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
SETTINGS$SECURITY_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
SETTINGS$SECURITY_ANALYZER_PLACEHOLDER = "SETTINGS$SECURITY_ANALYZER_PLACEHOLDER",
SETTINGS$DONT_KNOW_API_KEY = "SETTINGS$DONT_KNOW_API_KEY",
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
SETTINGS$SAVED = "SETTINGS$SAVED",
@@ -509,6 +513,8 @@ export enum I18nKey {
CONVERSATION$DELETE_WARNING = "CONVERSATION$DELETE_WARNING",
FEEDBACK$TITLE = "FEEDBACK$TITLE",
FEEDBACK$DESCRIPTION = "FEEDBACK$DESCRIPTION",
FEEDBACK$POSITIVE_SENT = "FEEDBACK$POSITIVE_SENT",
FEEDBACK$NEGATIVE_SENT = "FEEDBACK$NEGATIVE_SENT",
EXIT_PROJECT$WARNING = "EXIT_PROJECT$WARNING",
MODEL_SELECTOR$VERIFIED = "MODEL_SELECTOR$VERIFIED",
MODEL_SELECTOR$OTHERS = "MODEL_SELECTOR$OTHERS",
@@ -552,4 +558,24 @@ export enum I18nKey {
TIPS$PROTIP = "TIPS$PROTIP",
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
SETTINGS$NAV_USER = "SETTINGS$NAV_USER",
SETTINGS$USER_TITLE = "SETTINGS$USER_TITLE",
SETTINGS$USER_EMAIL = "SETTINGS$USER_EMAIL",
SETTINGS$USER_EMAIL_LOADING = "SETTINGS$USER_EMAIL_LOADING",
SETTINGS$SAVE = "SETTINGS$SAVE",
SETTINGS$EMAIL_SAVED_SUCCESSFULLY = "SETTINGS$EMAIL_SAVED_SUCCESSFULLY",
SETTINGS$EMAIL_VERIFIED_SUCCESSFULLY = "SETTINGS$EMAIL_VERIFIED_SUCCESSFULLY",
SETTINGS$FAILED_TO_SAVE_EMAIL = "SETTINGS$FAILED_TO_SAVE_EMAIL",
SETTINGS$SENDING = "SETTINGS$SENDING",
SETTINGS$VERIFICATION_EMAIL_SENT = "SETTINGS$VERIFICATION_EMAIL_SENT",
SETTINGS$EMAIL_VERIFICATION_REQUIRED = "SETTINGS$EMAIL_VERIFICATION_REQUIRED",
SETTINGS$INVALID_EMAIL_FORMAT = "SETTINGS$INVALID_EMAIL_FORMAT",
SETTINGS$EMAIL_VERIFICATION_RESTRICTION_MESSAGE = "SETTINGS$EMAIL_VERIFICATION_RESTRICTION_MESSAGE",
SETTINGS$RESEND_VERIFICATION = "SETTINGS$RESEND_VERIFICATION",
SETTINGS$FAILED_TO_RESEND_VERIFICATION = "SETTINGS$FAILED_TO_RESEND_VERIFICATION",
FEEDBACK$RATE_RESPONSE = "FEEDBACK$RATE_RESPONSE",
FEEDBACK$SELECT_REASON = "FEEDBACK$SELECT_REASON",
FEEDBACK$REASON_NOT_FOLLOW_INSTRUCTION = "FEEDBACK$REASON_NOT_FOLLOW_INSTRUCTION",
FEEDBACK$REASON_BAD_SOLUTION = "FEEDBACK$REASON_BAD_SOLUTION",
FEEDBACK$REASON_LACKS_ACCESS = "FEEDBACK$REASON_LACKS_ACCESS",
}

View File

@@ -1,4 +1,20 @@
{
"STATUS$WEBSOCKET_CLOSED": {
"en": "The WebSocket connection was closed.",
"ja": "WebSocket接続が閉じられました。",
"zh-CN": "WebSocket连接已关闭。",
"zh-TW": "WebSocket連接已關閉。",
"ko-KR": "WebSocket 연결이 닫혔습니다.",
"no": "WebSocket-tilkoblingen ble lukket.",
"it": "La connessione WebSocket è stata chiusa.",
"pt": "A conexão WebSocket foi fechada.",
"es": "La conexión WebSocket se ha cerrado.",
"ar": "تم إغلاق اتصال WebSocket.",
"fr": "La connexion WebSocket a été fermée.",
"tr": "WebSocket bağlantısı kapatıldı.",
"de": "Die WebSocket-Verbindung wurde geschlossen.",
"uk": "З'єднання WebSocket було закрито."
},
"HOME$LAUNCH_FROM_SCRATCH": {
"en": "Launch from Scratch",
"ja": "ゼロから始める",
@@ -2207,21 +2223,53 @@
"tr": "VS Code URL'si alınamadı",
"uk": "Не вдалося отримати VS Code URL"
},
"VSCODE$IFRAME_PERMISSIONS": {
"en": "clipboard-read; clipboard-write",
"ja": "clipboard-read; clipboard-write",
"zh-CN": "clipboard-read; clipboard-write",
"zh-TW": "clipboard-read; clipboard-write",
"ko-KR": "clipboard-read; clipboard-write",
"de": "clipboard-read; clipboard-write",
"no": "clipboard-read; clipboard-write",
"it": "clipboard-read; clipboard-write",
"pt": "clipboard-read; clipboard-write",
"es": "clipboard-read; clipboard-write",
"ar": "clipboard-read; clipboard-write",
"fr": "clipboard-read; clipboard-write",
"tr": "clipboard-read; clipboard-write",
"uk": "clipboard-read; clipboard-write"
"VSCODE$CROSS_ORIGIN_WARNING": {
"en": "The code editor cannot be embedded due to browser security restrictions. Cross-origin cookies are being blocked.",
"ja": "ブラウザのセキュリティ制限により、コードエディタを埋め込むことができません。クロスオリジンCookieがブロックされています。",
"zh-CN": "由于浏览器安全限制无法嵌入代码编辑器。跨源Cookie被阻止。",
"zh-TW": "由於瀏覽器安全限制無法嵌入代碼編輯器。跨源Cookie被阻止。",
"ko-KR": "브라우저 보안 제한으로 인해 코드 편집기를 삽입할 수 없습니다. 교차 출처 쿠키가 차단되고 있습니다.",
"de": "Der Code-Editor kann aufgrund von Browser-Sicherheitsbeschränkungen nicht eingebettet werden. Cross-Origin-Cookies werden blockiert.",
"no": "Koderedigereren kan ikke bygges inn på grunn av nettleserens sikkerhetsbegrensninger. Cross-origin cookies blir blokkert.",
"it": "L'editor di codice non può essere incorporato a causa delle restrizioni di sicurezza del browser. I cookie cross-origin vengono bloccati.",
"pt": "O editor de código não pode ser incorporado devido a restrições de segurança do navegador. Cookies de origem cruzada estão sendo bloqueados.",
"es": "El editor de código no se puede incrustar debido a las restricciones de seguridad del navegador. Las cookies de origen cruzado están siendo bloqueadas.",
"ar": "لا يمكن تضمين محرر التعليمات البرمجية بسبب قيود أمان المتصفح. يتم حظر ملفات تعريف الارتباط عبر المصدر.",
"fr": "L'éditeur de code ne peut pas être intégré en raison des restrictions de sécurité du navigateur. Les cookies cross-origin sont bloqués.",
"tr": "Tarayıcı güvenlik kısıtlamaları nedeniyle kod düzenleyici yerleştirilemiyor. Çapraz kaynaklı çerezler engelleniyor.",
"uk": "Редактор коду не може бути вбудований через обмеження безпеки браузера. Блокуються файли cookie з різних джерел."
},
"VSCODE$URL_PARSE_ERROR": {
"en": "Error parsing URL",
"ja": "URLの解析エラー",
"zh-CN": "URL解析错误",
"zh-TW": "URL解析錯誤",
"ko-KR": "URL 구문 분석 오류",
"de": "Fehler beim Parsen der URL",
"no": "Feil ved parsing av URL",
"it": "Errore durante l'analisi dell'URL",
"pt": "Erro ao analisar URL",
"es": "Error al analizar URL",
"ar": "خطأ في تحليل عنوان URL",
"fr": "Erreur d'analyse de l'URL",
"tr": "URL ayrıştırma hatası",
"uk": "Помилка аналізу URL"
},
"VSCODE$OPEN_IN_NEW_TAB": {
"en": "Open in New Tab",
"ja": "新しいタブで開く",
"zh-CN": "在新标签页中打开",
"zh-TW": "在新標籤頁中打開",
"ko-KR": "새 탭에서 열기",
"de": "In neuem Tab öffnen",
"no": "Åpne i ny fane",
"it": "Apri in una nuova scheda",
"pt": "Abrir em nova aba",
"es": "Abrir en nueva pestaña",
"ar": "فتح في علامة تبويب جديدة",
"fr": "Ouvrir dans un nouvel onglet",
"tr": "Yeni Sekmede Aç",
"uk": "Відкрити в новій вкладці"
},
"INCREASE_TEST_COVERAGE": {
"en": "Increase test coverage",
@@ -5311,6 +5359,22 @@
"ja": "セキュリティアナライザー",
"uk": "Увімкнути аналізатор безпеки"
},
"SETTINGS$SECURITY_ANALYZER_PLACEHOLDER":{
"en": "Select a security analyzer…",
"de": "Wählen Sie einen Sicherheitsanalysator aus…",
"zh-CN": "选择一个安全分析器…",
"zh-TW": "選擇一個安全分析器…",
"ko-KR": "보안 분석기를 선택하세요…",
"no": "Velg en sikkerhetsanalysator…",
"it": "Seleziona un analizzatore di sicurezza…",
"pt": "Selecione um analisador de segurança…",
"es": "Seleccione un analizador de seguridad…",
"ar": "اختر محلل الأمان…",
"fr": "Sélectionnez un analyseur de sécurité…",
"tr": "Bir güvenlik analizörü seçin…",
"ja": "セキュリティアナライザーを選択…",
"uk": "Виберіть аналізатор безпеки…"
},
"SETTINGS$DONT_KNOW_API_KEY": {
"en": "Don't know your API key?",
"ja": "APIキーがわかりませんか",
@@ -8143,6 +8207,38 @@
"de": "Wir schätzen Ihr Feedback. Bitte teilen Sie uns Ihre Gedanken mit.",
"uk": "Ми цінуємо ваш відгук. Будь ласка, поділіться з нами своїми думками."
},
"FEEDBACK$POSITIVE_SENT": {
"en": "Positive feedback sent",
"ja": "ポジティブなフィードバックが送信されました",
"zh-CN": "已发送积极反馈",
"zh-TW": "已發送積極反饋",
"ko-KR": "긍정적인 피드백이 전송되었습니다",
"no": "Positiv tilbakemelding sendt",
"ar": "تم إرسال تعليق إيجابي",
"de": "Positives Feedback gesendet",
"fr": "Commentaire positif envoyé",
"it": "Feedback positivo inviato",
"pt": "Feedback positivo enviado",
"es": "Comentario positivo enviado",
"tr": "Olumlu geri bildirim gönderildi",
"uk": "Позитивний відгук надіслано"
},
"FEEDBACK$NEGATIVE_SENT": {
"en": "Negative feedback sent",
"ja": "ネガティブなフィードバックが送信されました",
"zh-CN": "已发送消极反馈",
"zh-TW": "已發送消極反饋",
"ko-KR": "부정적인 피드백이 전송되었습니다",
"no": "Negativ tilbakemelding sendt",
"ar": "تم إرسال تعليق سلبي",
"de": "Negatives Feedback gesendet",
"fr": "Commentaire négatif envoyé",
"it": "Feedback negativo inviato",
"pt": "Feedback negativo enviado",
"es": "Comentario negativo enviado",
"tr": "Olumsuz geri bildirim gönderildi",
"uk": "Негативний відгук надіслано"
},
"EXIT_PROJECT$WARNING": {
"en": "Are you sure you want to exit this project? Any unsaved changes will be lost.",
"ja": "このプロジェクトを終了してもよろしいですか?保存されていない変更は失われます。",
@@ -8830,5 +8926,325 @@
"tr": "Geri bildirim gönderiliyor, lütfen bekleyin...",
"de": "Feedback senden, bitte warten...",
"uk": "Відправляємо відгук, будь ласка, почекайте..."
},
"SETTINGS$NAV_USER": {
"en": "User",
"ja": "ユーザー",
"zh-CN": "用户",
"zh-TW": "用戶",
"ko-KR": "사용자",
"no": "Bruker",
"it": "Utente",
"pt": "Usuário",
"es": "Usuario",
"ar": "المستخدم",
"fr": "Utilisateur",
"tr": "Kullanıcı",
"de": "Benutzer",
"uk": "Користувач"
},
"SETTINGS$USER_TITLE": {
"en": "User Information",
"ja": "ユーザー情報",
"zh-CN": "用户信息",
"zh-TW": "用戶信息",
"ko-KR": "사용자 정보",
"no": "Brukerinformasjon",
"it": "Informazioni utente",
"pt": "Informações do usuário",
"es": "Información del usuario",
"ar": "معلومات المستخدم",
"fr": "Informations utilisateur",
"tr": "Kullanıcı Bilgileri",
"de": "Benutzerinformationen",
"uk": "Інформація про користувача"
},
"SETTINGS$USER_EMAIL": {
"en": "Email",
"ja": "メール",
"zh-CN": "邮箱",
"zh-TW": "郵箱",
"ko-KR": "이메일",
"no": "E-post",
"it": "Email",
"pt": "Email",
"es": "Correo electrónico",
"ar": "البريد الإلكتروني",
"fr": "Email",
"tr": "E-posta",
"de": "E-Mail",
"uk": "Електронна пошта"
},
"SETTINGS$USER_EMAIL_LOADING": {
"en": "Loading...",
"ja": "読み込み中...",
"zh-CN": "加载中...",
"zh-TW": "加載中...",
"ko-KR": "로딩 중...",
"no": "Laster...",
"it": "Caricamento...",
"pt": "Carregando...",
"es": "Cargando...",
"ar": "جار التحميل...",
"fr": "Chargement...",
"tr": "Yükleniyor...",
"de": "Wird geladen...",
"uk": "Завантаження..."
},
"SETTINGS$SAVE": {
"en": "Save",
"ja": "保存",
"zh-CN": "保存",
"zh-TW": "儲存",
"ko-KR": "저장",
"no": "Lagre",
"it": "Salva",
"pt": "Salvar",
"es": "Guardar",
"ar": "حفظ",
"fr": "Enregistrer",
"tr": "Kaydet",
"de": "Speichern",
"uk": "Зберегти"
},
"SETTINGS$EMAIL_SAVED_SUCCESSFULLY": {
"en": "Email saved successfully",
"ja": "メールが正常に保存されました",
"zh-CN": "邮箱保存成功",
"zh-TW": "郵箱儲存成功",
"ko-KR": "이메일이 성공적으로 저장되었습니다",
"no": "E-post lagret",
"it": "Email salvata con successo",
"pt": "Email salvo com sucesso",
"es": "Correo electrónico guardado con éxito",
"ar": "تم حفظ البريد الإلكتروني بنجاح",
"fr": "Email enregistré avec succès",
"tr": "E-posta başarıyla kaydedildi",
"de": "E-Mail erfolgreich gespeichert",
"uk": "Електронну пошту успішно збережено"
},
"SETTINGS$EMAIL_VERIFIED_SUCCESSFULLY": {
"en": "Your email has been verified successfully!",
"ja": "メールアドレスの確認が完了しました!",
"zh-CN": "您的邮箱已成功验证!",
"zh-TW": "您的郵箱已成功驗證!",
"ko-KR": "이메일이 성공적으로 인증되었습니다!",
"no": "E-posten din er bekreftet!",
"it": "La tua email è stata verificata con successo!",
"pt": "Seu email foi verificado com sucesso!",
"es": "¡Tu correo electrónico ha sido verificado con éxito!",
"ar": "تم التحقق من بريدك الإلكتروني بنجاح!",
"fr": "Votre email a été vérifié avec succès !",
"tr": "E-postanız başarıyla doğrulandı!",
"de": "Ihre E-Mail wurde erfolgreich verifiziert!",
"uk": "Вашу електронну пошту успішно підтверджено!"
},
"SETTINGS$FAILED_TO_SAVE_EMAIL": {
"en": "Failed to save email",
"ja": "メールの保存に失敗しました",
"zh-CN": "保存邮箱失败",
"zh-TW": "儲存郵箱失敗",
"ko-KR": "이메일 저장 실패",
"no": "Kunne ikke lagre e-post",
"it": "Impossibile salvare l'email",
"pt": "Falha ao salvar email",
"es": "Error al guardar el correo electrónico",
"ar": "فشل في حفظ البريد الإلكتروني",
"fr": "Échec de l'enregistrement de l'email",
"tr": "E-posta kaydedilemedi",
"de": "E-Mail konnte nicht gespeichert werden",
"uk": "Не вдалося зберегти електронну пошту"
},
"SETTINGS$SENDING": {
"en": "Sending",
"ja": "送信中",
"zh-CN": "发送中",
"zh-TW": "發送中",
"ko-KR": "전송 중",
"no": "Sender",
"it": "Invio in corso",
"pt": "Enviando",
"es": "Enviando",
"ar": "جاري الإرسال",
"fr": "Envoi en cours",
"tr": "Gönderiliyor",
"de": "Wird gesendet",
"uk": "Надсилання"
},
"SETTINGS$VERIFICATION_EMAIL_SENT": {
"en": "Verification email sent",
"ja": "確認メールを送信しました",
"zh-CN": "验证邮件已发送",
"zh-TW": "驗證郵件已發送",
"ko-KR": "인증 이메일이 전송되었습니다",
"no": "Bekreftelsese-post sendt",
"it": "Email di verifica inviata",
"pt": "Email de verificação enviado",
"es": "Correo de verificación enviado",
"ar": "تم إرسال بريد التحقق",
"fr": "Email de vérification envoyé",
"tr": "Doğrulama e-postası gönderildi",
"de": "Bestätigungs-E-Mail gesendet",
"uk": "Лист підтвердження надіслано"
},
"SETTINGS$EMAIL_VERIFICATION_REQUIRED": {
"en": "You must verify your email address before using All Hands",
"ja": "All Handsを使用する前にメールアドレスを確認する必要があります",
"zh-CN": "使用All Hands前您必须验证您的电子邮件地址",
"zh-TW": "使用All Hands前您必須驗證您的電子郵件地址",
"ko-KR": "All Hands를 사용하기 전에 이메일 주소를 확인해야 합니다",
"no": "Du må bekrefte e-postadressen din før du bruker All Hands",
"it": "Devi verificare il tuo indirizzo email prima di utilizzare All Hands",
"pt": "Você deve verificar seu endereço de e-mail antes de usar o All Hands",
"es": "Debe verificar su dirección de correo electrónico antes de usar All Hands",
"ar": "يجب عليك التحقق من عنوان بريدك الإلكتروني قبل استخدام All Hands",
"fr": "Vous devez vérifier votre adresse e-mail avant d'utiliser All Hands",
"tr": "All Hands'i kullanmadan önce e-posta adresinizi doğrulamanız gerekiyor",
"de": "Sie müssen Ihre E-Mail-Adresse bestätigen, bevor Sie All Hands verwenden können",
"uk": "Ви повинні підтвердити свою електронну адресу перед використанням All Hands"
},
"SETTINGS$INVALID_EMAIL_FORMAT": {
"en": "Please enter a valid email address",
"ja": "有効なメールアドレスを入力してください",
"zh-CN": "请输入有效的电子邮件地址",
"zh-TW": "請輸入有效的電子郵件地址",
"ko-KR": "유효한 이메일 주소를 입력하세요",
"no": "Vennligst skriv inn en gyldig e-postadresse",
"it": "Inserisci un indirizzo email valido",
"pt": "Por favor, insira um endereço de e-mail válido",
"es": "Por favor, introduzca una dirección de correo electrónico válida",
"ar": "الرجاء إدخال عنوان بريد إلكتروني صالح",
"fr": "Veuillez entrer une adresse e-mail valide",
"tr": "Lütfen geçerli bir e-posta adresi girin",
"de": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"uk": "Будь ласка, введіть дійсну електронну адресу"
},
"SETTINGS$EMAIL_VERIFICATION_RESTRICTION_MESSAGE": {
"en": "Your access is limited until your email is verified. You can only access this settings page.",
"ja": "メールが確認されるまでアクセスが制限されています。この設定ページにのみアクセスできます。",
"zh-CN": "在验证您的电子邮件之前,您的访问权限受到限制。您只能访问此设置页面。",
"zh-TW": "在驗證您的電子郵件之前,您的訪問權限受到限制。您只能訪問此設置頁面。",
"ko-KR": "이메일이 확인될 때까지 액세스가 제한됩니다. 이 설정 페이지만 액세스할 수 있습니다.",
"no": "Din tilgang er begrenset til e-posten din er bekreftet. Du kan bare få tilgang til denne innstillingssiden.",
"it": "Il tuo accesso è limitato fino a quando la tua email non viene verificata. Puoi accedere solo a questa pagina delle impostazioni.",
"pt": "Seu acesso é limitado até que seu e-mail seja verificado. Você só pode acessar esta página de configurações.",
"es": "Su acceso es limitado hasta que se verifique su correo electrónico. Solo puede acceder a esta página de configuración.",
"ar": "وصولك محدود حتى يتم التحقق من بريدك الإلكتروني. يمكنك فقط الوصول إلى صفحة الإعدادات هذه.",
"fr": "Votre accès est limité jusqu'à ce que votre e-mail soit vérifié. Vous ne pouvez accéder qu'à cette page de paramètres.",
"tr": "E-postanız doğrulanana kadar erişiminiz sınırlıdır. Yalnızca bu ayarlar sayfasına erişebilirsiniz.",
"de": "Ihr Zugriff ist eingeschränkt, bis Ihre E-Mail-Adresse bestätigt wurde. Sie können nur auf diese Einstellungsseite zugreifen.",
"uk": "Ваш доступ обмежений, доки ваша електронна пошта не буде підтверджена. Ви можете отримати доступ лише до цієї сторінки налаштувань."
},
"SETTINGS$RESEND_VERIFICATION": {
"en": "Resend verification",
"ja": "確認メールを再送信",
"zh-CN": "重新发送验证",
"zh-TW": "重新發送驗證",
"ko-KR": "인증 재전송",
"no": "Send bekreftelse på nytt",
"it": "Rinvia verifica",
"pt": "Reenviar verificação",
"es": "Reenviar verificación",
"ar": "إعادة إرسال التحقق",
"fr": "Renvoyer la vérification",
"tr": "Doğrulamayı yeniden gönder",
"de": "Bestätigung erneut senden",
"uk": "Надіслати підтвердження повторно"
},
"SETTINGS$FAILED_TO_RESEND_VERIFICATION": {
"en": "Failed to resend verification email",
"ja": "確認メールの再送信に失敗しました",
"zh-CN": "重新发送验证邮件失败",
"zh-TW": "重新發送驗證郵件失敗",
"ko-KR": "인증 이메일 재전송 실패",
"no": "Kunne ikke sende bekreftelsese-post på nytt",
"it": "Impossibile rinviare l'email di verifica",
"pt": "Falha ao reenviar email de verificação",
"es": "Error al reenviar el correo de verificación",
"ar": "فشل في إعادة إرسال بريد التحقق",
"fr": "Échec du renvoi de l'email de vérification",
"tr": "Doğrulama e-postası yeniden gönderilemedi",
"de": "Bestätigungs-E-Mail konnte nicht erneut gesendet werden",
"uk": "Не вдалося повторно надіслати лист підтвердження"
},
"FEEDBACK$RATE_RESPONSE": {
"en": "Rate this response:",
"de": "Bewerten Sie diese Antwort:",
"it": "Valuta questa risposta:",
"pt": "Avalie esta resposta:",
"es": "Califica esta respuesta:",
"ja": "この回答を評価してください:",
"zh-CN": "评价此回复:",
"zh-TW": "評價此回覆:",
"ko-KR": "이 응답을 평가하세요:",
"no": "Vurder dette svaret:",
"ar": "قيم هذه الإجابة:",
"fr": "Évaluez cette réponse:",
"tr": "Bu yanıtı değerlendirin:",
"uk": "Оцініть цю відповідь:"
},
"FEEDBACK$SELECT_REASON": {
"en": "Please select a reason:",
"de": "Bitte wählen Sie einen Grund:",
"it": "Seleziona un motivo:",
"pt": "Por favor, selecione um motivo:",
"es": "Por favor, seleccione un motivo:",
"ja": "理由を選択してください:",
"zh-CN": "请选择原因:",
"zh-TW": "請選擇原因:",
"ko-KR": "이유를 선택해 주세요:",
"no": "Vennligst velg en grunn:",
"ar": "الرجاء اختيار سبب:",
"fr": "Veuillez sélectionner une raison:",
"tr": "Lütfen bir neden seçin:",
"uk": "Будь ласка, виберіть причину:"
},
"FEEDBACK$REASON_NOT_FOLLOW_INSTRUCTION": {
"en": "The agent did not follow my instruction",
"de": "Der Agent hat meine Anweisung nicht befolgt",
"it": "L'agente non ha seguito le mie istruzioni",
"pt": "O agente não seguiu minhas instruções",
"es": "El agente no siguió mis instrucciones",
"ja": "エージェントが私の指示に従わなかった",
"zh-CN": "代理未遵循我的指示",
"zh-TW": "代理未遵循我的指示",
"ko-KR": "에이전트가 내 지시를 따르지 않았습니다",
"no": "Agenten fulgte ikke instruksjonene mine",
"ar": "لم يتبع الوكيل تعليماتي",
"fr": "L'agent n'a pas suivi mes instructions",
"tr": "Ajan talimatlarımı takip etmedi",
"uk": "Агент не дотримувався моїх інструкцій"
},
"FEEDBACK$REASON_BAD_SOLUTION": {
"en": "The agent did not implement a good solution",
"de": "Der Agent hat keine gute Lösung implementiert",
"it": "L'agente non ha implementato una buona soluzione",
"pt": "O agente não implementou uma boa solução",
"es": "El agente no implementó una buena solución",
"ja": "エージェントが良い解決策を実装しなかった",
"zh-CN": "代理未实现良好的解决方案",
"zh-TW": "代理未實現良好的解決方案",
"ko-KR": "에이전트가 좋은 해결책을 구현하지 않았습니다",
"no": "Agenten implementerte ikke en god løsning",
"ar": "لم ينفذ الوكيل حلاً جيدًا",
"fr": "L'agent n'a pas implémenté une bonne solution",
"tr": "Ajan iyi bir çözüm uygulamadı",
"uk": "Агент не реалізував хороше рішення"
},
"FEEDBACK$REASON_LACKS_ACCESS": {
"en": "The agent lacks access to software or hardware that is not installable in the runtime to complete the task",
"de": "Dem Agenten fehlt der Zugriff auf Software oder Hardware, die in der Laufzeitumgebung nicht installierbar ist, um die Aufgabe zu erledigen",
"it": "L'agente non ha accesso a software o hardware non installabile nel runtime per completare l'attività",
"pt": "O agente não tem acesso a software ou hardware que não é instalável no tempo de execução para concluir a tarefa",
"es": "El agente no tiene acceso a software o hardware que no se puede instalar en el entorno de ejecución para completar la tarea",
"ja": "エージェントはタスクを完了するためにランタイムにインストールできないソフトウェアまたはハードウェアへのアクセスが不足しています",
"zh-CN": "代理缺乏访问无法在运行时安装的软件或硬件来完成任务",
"zh-TW": "代理缺乏訪問無法在運行時安裝的軟件或硬件來完成任務",
"ko-KR": "에이전트는 런타임에 설치할 수 없는 소프트웨어나 하드웨어에 접근할 수 없어 작업을 완료할 수 없습니다",
"no": "Agenten mangler tilgang til programvare eller maskinvare som ikke kan installeres i kjøretidsmiljøet for å fullføre oppgaven",
"ar": "يفتقر الوكيل إلى الوصول إلى البرامج أو الأجهزة التي لا يمكن تثبيتها في وقت التشغيل لإكمال المهمة",
"fr": "L'agent n'a pas accès à des logiciels ou du matériel qui ne peuvent pas être installés dans l'environnement d'exécution pour accomplir la tâche",
"tr": "Ajan, görevi tamamlamak için çalışma zamanında yüklenemeyen yazılım veya donanıma erişim eksikliği yaşıyor",
"uk": "Агент не має доступу до програмного або апаратного забезпечення, яке неможливо встановити в середовищі виконання для виконання завдання"
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L439.5 329 543.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M287.9 0c9.2 0 17.6 5.2 21.6 13.5l68.6 141.3 153.2 22.6c9 1.3 16.5 7.6 19.3 16.3s.5 18.1-5.9 24.5L433.6 328.4l26.2 155.6c1.5 9-2.2 18.1-9.6 23.5s-17.3 6-25.3 1.7l-137-73.2L151 509.1c-8.1 4.3-17.9 3.7-25.3-1.7s-11.2-14.5-9.7-23.5l26.2-155.6L31.1 218.2c-6.5-6.4-8.7-15.9-5.9-24.5s10.3-14.9 19.3-16.3l153.2-22.6L266.3 13.5C270.4 5.2 278.7 0 287.9 0zm0 79L235.4 187.2c-3.5 7.1-10.2 12.1-18.1 13.3L99 217.9 184.9 303c5.5 5.5 8.1 13.3 6.8 21L171.4 443.7l105.2-56.2c7.1-3.8 15.6-3.8 22.6 0l105.2 56.2L384.2 324.1c-1.3-7.7 1.2-15.5 6.8-21l85.9-85.1L358.6 200.5c-7.8-1.2-14.6-6.1-18.1-13.3L287.9 79z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -12,6 +12,7 @@ export type Message = {
pending?: boolean;
translationID?: string;
eventID?: number;
feedback?: "positive" | "negative" | null;
observation?: PayloadAction<OpenHandsObservation>;
action?: PayloadAction<OpenHandsAction>;
};

View File

@@ -12,6 +12,7 @@ export default [
route("settings", "routes/settings.tsx", [
index("routes/llm-settings.tsx"),
route("mcp", "routes/mcp-settings.tsx"),
route("user", "routes/user-settings.tsx"),
route("git", "routes/git-settings.tsx"),
route("app", "routes/app-settings.tsx"),
route("billing", "routes/billing.tsx"),

View File

@@ -43,7 +43,7 @@ function AppContent() {
const { t } = useTranslation();
const { data: settings } = useSettings();
const { conversationId } = useConversationId();
const { data: conversation, isFetched } = useActiveConversation();
const { data: conversation, isFetched, refetch } = useActiveConversation();
const { data: isAuthed } = useIsAuthed();
const { curAgentState } = useSelector((state: RootState) => state.agent);
@@ -61,8 +61,13 @@ function AppContent() {
"This conversation does not exist, or you do not have permission to access it.",
);
navigate("/");
} else if (conversation?.status === "STOPPED") {
// start the conversation if the state is stopped on initial load
OpenHands.startConversation(conversation.conversation_id).then(() =>
refetch(),
);
}
}, [conversation, isFetched, isAuthed]);
}, [conversation?.conversation_id, isFetched, isAuthed]);
React.useEffect(() => {
dispatch(clearTerminal());

View File

@@ -469,6 +469,9 @@ function LlmSettingsScreen() {
label: analyzer,
})) || []
}
placeholder={t(
I18nKey.SETTINGS$SECURITY_ANALYZER_PLACEHOLDER,
)}
defaultSelectedKey={settings.SECURITY_ANALYZER}
isClearable
showOptionalTag

View File

@@ -25,6 +25,7 @@ import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { useAutoLogin } from "#/hooks/use-auto-login";
import { useAuthCallback } from "#/hooks/use-auth-callback";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
import { EmailVerificationGuard } from "#/components/features/guards/email-verification-guard";
export function ErrorBoundary() {
const error = useRouteError();
@@ -204,7 +205,9 @@ export default function MainApp() {
id="root-outlet"
className="h-[calc(100%-50px)] md:h-full w-full relative overflow-auto"
>
<Outlet />
<EmailVerificationGuard>
<Outlet />
</EmailVerificationGuard>
</div>
{renderAuthModal && (

View File

@@ -15,6 +15,7 @@ function SettingsScreen() {
const isSaas = config?.APP_MODE === "saas";
const saasNavItems = [
{ to: "/settings/user", text: t("SETTINGS$NAV_USER") },
{ to: "/settings/git", text: t("SETTINGS$NAV_GIT") },
{ to: "/settings/app", text: t("SETTINGS$NAV_APPLICATION") },
{ to: "/settings/billing", text: t("SETTINGS$NAV_CREDITS") },
@@ -33,10 +34,11 @@ function SettingsScreen() {
React.useEffect(() => {
if (isSaas) {
if (pathname === "/settings") {
navigate("/settings/git");
navigate("/settings/user");
}
} else {
const noEnteringPaths = [
"/settings/user",
"/settings/billing",
"/settings/credits",
"/settings/api-keys",

View File

@@ -0,0 +1,229 @@
import React, { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { useSettings } from "#/hooks/query/use-settings";
import { openHands } from "#/api/open-hands-axios";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
// Email validation regex pattern
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
function EmailInputSection({
email,
onEmailChange,
onSaveEmail,
onResendVerification,
isSaving,
isResendingVerification,
isEmailChanged,
emailVerified,
isEmailValid,
children,
}: {
email: string;
onEmailChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSaveEmail: () => void;
onResendVerification: () => void;
isSaving: boolean;
isResendingVerification: boolean;
isEmailChanged: boolean;
emailVerified?: boolean;
isEmailValid: boolean;
children: React.ReactNode;
}) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm">{t("SETTINGS$USER_EMAIL")}</label>
<div className="flex items-center gap-3">
<input
type="email"
value={email}
onChange={onEmailChange}
className={`text-base text-white p-2 bg-base-tertiary rounded border ${
isEmailChanged && !isEmailValid
? "border-red-500"
: "border-tertiary"
} flex-grow focus:outline-none focus:border-transparent focus:ring-0`}
placeholder={t("SETTINGS$USER_EMAIL_LOADING")}
data-testid="email-input"
/>
</div>
{isEmailChanged && !isEmailValid && (
<div
className="text-red-500 text-sm mt-1"
data-testid="email-validation-error"
>
{t("SETTINGS$INVALID_EMAIL_FORMAT")}
</div>
)}
<div className="flex items-center gap-3 mt-2">
<button
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]"
data-testid="save-email-button"
>
{isSaving ? t("SETTINGS$SAVING") : t("SETTINGS$SAVE")}
</button>
{emailVerified === false && (
<button
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]"
data-testid="resend-verification-button"
>
{isResendingVerification
? t("SETTINGS$SENDING")
: t("SETTINGS$RESEND_VERIFICATION")}
</button>
)}
</div>
{children}
</div>
</div>
);
}
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"
role="alert"
>
<p className="font-bold">{t("SETTINGS$EMAIL_VERIFICATION_REQUIRED")}</p>
<p className="text-sm">
{t("SETTINGS$EMAIL_VERIFICATION_RESTRICTION_MESSAGE")}
</p>
</div>
);
}
// These components have been replaced with toast notifications
function UserSettingsScreen() {
const { t } = useTranslation();
const { data: settings, isLoading, refetch } = useSettings();
const [email, setEmail] = useState("");
const [originalEmail, setOriginalEmail] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [isResendingVerification, setIsResendingVerification] = useState(false);
const [isEmailValid, setIsEmailValid] = useState(true);
const queryClient = useQueryClient();
const pollingIntervalRef = useRef<number | null>(null);
const prevVerificationStatusRef = useRef<boolean | undefined>(undefined);
useEffect(() => {
if (settings?.EMAIL) {
setEmail(settings.EMAIL);
setOriginalEmail(settings.EMAIL);
setIsEmailValid(EMAIL_REGEX.test(settings.EMAIL));
}
}, [settings?.EMAIL]);
useEffect(() => {
if (pollingIntervalRef.current) {
window.clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
if (
prevVerificationStatusRef.current === false &&
settings?.EMAIL_VERIFIED === true
) {
// Display toast notification instead of setting state
displaySuccessToast(t("SETTINGS$EMAIL_VERIFIED_SUCCESSFULLY"));
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["settings"] });
}, 2000);
}
prevVerificationStatusRef.current = settings?.EMAIL_VERIFIED;
if (settings?.EMAIL_VERIFIED === false) {
pollingIntervalRef.current = window.setInterval(() => {
refetch();
}, 5000);
}
return () => {
if (pollingIntervalRef.current) {
window.clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}, [settings?.EMAIL_VERIFIED, refetch, queryClient, t]);
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newEmail = e.target.value;
setEmail(newEmail);
setIsEmailValid(EMAIL_REGEX.test(newEmail));
};
const handleSaveEmail = async () => {
if (email === originalEmail || !isEmailValid) return;
try {
setIsSaving(true);
await openHands.post("/api/email", { email }, { withCredentials: true });
setOriginalEmail(email);
// Display toast notification instead of setting state
displaySuccessToast(t("SETTINGS$EMAIL_SAVED_SUCCESSFULLY"));
queryClient.invalidateQueries({ queryKey: ["settings"] });
} catch (error) {
// eslint-disable-next-line no-console
console.error(t("SETTINGS$FAILED_TO_SAVE_EMAIL"), error);
} finally {
setIsSaving(false);
}
};
const handleResendVerification = async () => {
try {
setIsResendingVerification(true);
await openHands.put("/api/email/verify", {}, { withCredentials: true });
// Display toast notification instead of setting state
displaySuccessToast(t("SETTINGS$VERIFICATION_EMAIL_SENT"));
} catch (error) {
// eslint-disable-next-line no-console
console.error(t("SETTINGS$FAILED_TO_RESEND_VERIFICATION"), error);
} finally {
setIsResendingVerification(false);
}
};
const isEmailChanged = email !== originalEmail;
return (
<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" />
) : (
<EmailInputSection
email={email}
onEmailChange={handleEmailChange}
onSaveEmail={handleSaveEmail}
onResendVerification={handleResendVerification}
isSaving={isSaving}
isResendingVerification={isResendingVerification}
isEmailChanged={isEmailChanged}
emailVerified={settings?.EMAIL_VERIFIED}
isEmailValid={isEmailValid}
>
{settings?.EMAIL_VERIFIED === false && <VerificationAlert />}
</EmailInputSection>
)}
</div>
</div>
);
}
export default UserSettingsScreen;

View File

@@ -1,10 +1,11 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
import { VSCODE_IN_NEW_TAB } from "#/utils/feature-flags";
function VSCodeTab() {
const { t } = useTranslation();
@@ -12,6 +13,31 @@ function VSCodeTab() {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
const iframeRef = React.useRef<HTMLIFrameElement>(null);
const [isCrossProtocol, setIsCrossProtocol] = useState(false);
const [iframeError, setIframeError] = useState<string | null>(null);
useEffect(() => {
if (data?.url) {
try {
const iframeProtocol = new URL(data.url).protocol;
const currentProtocol = window.location.protocol;
// Check if the iframe URL has a different protocol than the current page
setIsCrossProtocol(
VSCODE_IN_NEW_TAB() || iframeProtocol !== currentProtocol,
);
} catch (e) {
// Silently handle URL parsing errors
setIframeError(t("VSCODE$URL_PARSE_ERROR"));
}
}
}, [data?.url]);
const handleOpenInNewTab = () => {
if (data?.url) {
window.open(data.url, "_blank", "noopener,noreferrer");
}
};
if (isRuntimeInactive) {
return (
@@ -29,14 +55,36 @@ function VSCodeTab() {
);
}
if (error || (data && data.error) || !data?.url) {
if (error || (data && data.error) || !data?.url || iframeError) {
return (
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
{data?.error || String(error) || t(I18nKey.VSCODE$URL_NOT_AVAILABLE)}
{iframeError ||
data?.error ||
String(error) ||
t(I18nKey.VSCODE$URL_NOT_AVAILABLE)}
</div>
);
}
// If cross-origin, show a button to open in new tab
if (isCrossProtocol) {
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="text-xl text-tertiary-light text-center max-w-md">
{t("VSCODE$CROSS_ORIGIN_WARNING")}
</div>
<button
type="button"
onClick={handleOpenInNewTab}
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark transition-colors"
>
{t("VSCODE$OPEN_IN_NEW_TAB")}
</button>
</div>
);
}
// If same origin, use the iframe
return (
<div className="h-full w-full">
<iframe
@@ -44,7 +92,7 @@ function VSCodeTab() {
title={t(I18nKey.VSCODE$TITLE)}
src={data.url}
className="w-full h-full border-0"
allow={t(I18nKey.VSCODE$IFRAME_PERMISSIONS)}
allow="clipboard-read; clipboard-write"
/>
</div>
);

View File

@@ -11,3 +11,23 @@ export function createChatMessage(
};
return event;
}
export function createUserFeedback(
feedbackType: "positive" | "negative",
targetType: "message" | "trajectory",
targetId?: number,
rating?: number,
reason?: string | null,
) {
const event = {
action: ActionType.USER_FEEDBACK,
args: {
feedback_type: feedbackType,
target_type: targetType,
target_id: targetId,
rating,
reason,
},
};
return event;
}

View File

@@ -19,6 +19,8 @@ export const DEFAULT_SETTINGS: Settings = {
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
SEARCH_API_KEY: "",
IS_NEW_USER: true,
EMAIL: "",
EMAIL_VERIFIED: true, // Default to true to avoid restricting access unnecessarily
MCP_CONFIG: {
sse_servers: [],
stdio_servers: [],

View File

@@ -0,0 +1,398 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { Message } from "#/message";
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsEventType } from "#/types/core/base";
import {
CommandObservation,
IPythonObservation,
OpenHandsObservation,
RecallObservation,
} from "#/types/core/observations";
type SliceState = {
messages: Message[];
systemMessage: {
content: string;
tools: Array<Record<string, unknown>> | null;
openhands_version: string | null;
agent_class: string | null;
} | null;
};
const MAX_CONTENT_LENGTH = 1000;
const HANDLED_ACTIONS: OpenHandsEventType[] = [
"run",
"run_ipython",
"write",
"read",
"browse",
"browse_interactive",
"edit",
"user_feedback",
"recall",
"think",
"system",
"call_tool_mcp",
"mcp",
];
function getRiskText(risk: ActionSecurityRisk) {
switch (risk) {
case ActionSecurityRisk.LOW:
return "Low Risk";
case ActionSecurityRisk.MEDIUM:
return "Medium Risk";
case ActionSecurityRisk.HIGH:
return "High Risk";
case ActionSecurityRisk.UNKNOWN:
default:
return "Unknown Risk";
}
}
const initialState: SliceState = {
messages: [],
systemMessage: null,
};
export const chatSlice = createSlice({
name: "chat",
initialState,
reducers: {
addUserMessage(
state,
action: PayloadAction<{
content: string;
imageUrls: string[];
timestamp: string;
pending?: boolean;
}>,
) {
const message: Message = {
type: "thought",
sender: "user",
content: action.payload.content,
imageUrls: action.payload.imageUrls,
timestamp: action.payload.timestamp || new Date().toISOString(),
pending: !!action.payload.pending,
};
// Remove any pending messages
let i = state.messages.length;
while (i) {
i -= 1;
const m = state.messages[i] as Message;
if (m.pending) {
state.messages.splice(i, 1);
}
}
state.messages.push(message);
},
addAssistantMessage(state: SliceState, action: PayloadAction<string>) {
const message: Message = {
type: "thought",
sender: "assistant",
content: action.payload,
imageUrls: [],
timestamp: new Date().toISOString(),
pending: false,
};
state.messages.push(message);
},
addAssistantAction(
state: SliceState,
action: PayloadAction<OpenHandsAction>,
) {
const actionID = action.payload.action;
if (!HANDLED_ACTIONS.includes(actionID)) {
return;
}
const translationID = `ACTION_MESSAGE$${actionID.toUpperCase()}`;
let text = "";
if (actionID === "system") {
// Store the system message in the state
state.systemMessage = {
content: action.payload.args.content,
tools: action.payload.args.tools,
openhands_version: action.payload.args.openhands_version,
agent_class: action.payload.args.agent_class,
};
// Don't add a message for system actions
return;
}
if (actionID === "run") {
text = `Command:\n\`${action.payload.args.command}\``;
} else if (actionID === "run_ipython") {
text = `\`\`\`\n${action.payload.args.code}\n\`\`\``;
} else if (actionID === "write") {
let { content } = action.payload.args;
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
}
text = `${action.payload.args.path}\n${content}`;
} else if (actionID === "browse") {
text = `Browsing ${action.payload.args.url}`;
} else if (actionID === "browse_interactive") {
// Include the browser_actions in the content
text = `**Action:**\n\n\`\`\`python\n${action.payload.args.browser_actions}\n\`\`\``;
} else if (actionID === "recall") {
// skip recall actions
return;
} else if (actionID === "call_tool_mcp") {
// Format MCP action with name and arguments
const name = action.payload.args.name || "";
const args = action.payload.args.arguments || {};
text = `**MCP Tool Call:** ${name}\n\n`;
// Include thought if available
if (action.payload.args.thought) {
text += `\n\n**Thought:**\n${action.payload.args.thought}`;
}
text += `\n\n**Arguments:**\n\`\`\`json\n${JSON.stringify(args, null, 2)}\n\`\`\``;
}
if (actionID === "run" || actionID === "run_ipython") {
if (
action.payload.args.confirmation_state === "awaiting_confirmation"
) {
text += `\n\n${getRiskText(action.payload.args.security_risk as unknown as ActionSecurityRisk)}`;
}
} else if (actionID === "think") {
text = action.payload.args.thought;
}
const message: Message = {
type: "action",
sender: "assistant",
translationID,
eventID: action.payload.id,
content: text,
imageUrls: [],
timestamp: new Date().toISOString(),
action,
};
state.messages.push(message);
},
addAssistantObservation(
state: SliceState,
observation: PayloadAction<OpenHandsObservation>,
) {
const observationID = observation.payload.observation;
if (!HANDLED_ACTIONS.includes(observationID)) {
return;
}
// Special handling for RecallObservation - create a new message instead of updating an existing one
if (observationID === "recall") {
const recallObs = observation.payload as RecallObservation;
let content = ``;
// Handle workspace context
if (recallObs.extras.recall_type === "workspace_context") {
if (recallObs.extras.repo_name) {
content += `\n\n**Repository:** ${recallObs.extras.repo_name}`;
}
if (recallObs.extras.repo_directory) {
content += `\n\n**Directory:** ${recallObs.extras.repo_directory}`;
}
if (recallObs.extras.date) {
content += `\n\n**Date:** ${recallObs.extras.date}`;
}
if (
recallObs.extras.runtime_hosts &&
Object.keys(recallObs.extras.runtime_hosts).length > 0
) {
content += `\n\n**Available Hosts**`;
for (const [host, port] of Object.entries(
recallObs.extras.runtime_hosts,
)) {
content += `\n\n- ${host} (port ${port})`;
}
}
if (
recallObs.extras.custom_secrets_descriptions &&
Object.keys(recallObs.extras.custom_secrets_descriptions).length > 0
) {
content += `\n\n**Custom Secrets**`;
for (const [name, description] of Object.entries(
recallObs.extras.custom_secrets_descriptions,
)) {
content += `\n\n- $${name}: ${description}`;
}
}
if (recallObs.extras.repo_instructions) {
content += `\n\n**Repository Instructions:**\n\n${recallObs.extras.repo_instructions}`;
}
if (recallObs.extras.additional_agent_instructions) {
content += `\n\n**Additional Instructions:**\n\n${recallObs.extras.additional_agent_instructions}`;
}
}
// Create a new message for the observation
// Use the correct translation ID format that matches what's in the i18n file
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
// Handle microagent knowledge
if (
recallObs.extras.microagent_knowledge &&
recallObs.extras.microagent_knowledge.length > 0
) {
content += `\n\n**Triggered Microagent Knowledge:**`;
for (const knowledge of recallObs.extras.microagent_knowledge) {
content += `\n\n- **${knowledge.name}** (triggered by keyword: ${knowledge.trigger})\n\n\`\`\`\n${knowledge.content}\n\`\`\``;
}
}
const message: Message = {
type: "action",
sender: "assistant",
translationID,
eventID: observation.payload.id,
content,
imageUrls: [],
timestamp: new Date().toISOString(),
success: true,
};
state.messages.push(message);
return; // Skip the normal observation handling below
}
// Normal handling for other observation types
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
const causeID = observation.payload.cause;
const causeMessage = state.messages.find(
(message) => message.eventID === causeID,
);
if (!causeMessage) {
return;
}
causeMessage.translationID = translationID;
causeMessage.observation = observation;
// Set success property based on observation type
if (observationID === "run") {
const commandObs = observation.payload as CommandObservation;
// If exit_code is -1, it means the command timed out, so we set success to undefined
// to not show any status indicator
if (commandObs.extras.metadata.exit_code === -1) {
causeMessage.success = undefined;
} else {
causeMessage.success = commandObs.extras.metadata.exit_code === 0;
}
} else if (observationID === "run_ipython") {
// For IPython, we consider it successful if there's no error message
const ipythonObs = observation.payload as IPythonObservation;
causeMessage.success = !ipythonObs.content
.toLowerCase()
.includes("error:");
} else if (observationID === "read" || observationID === "edit") {
// For read/edit operations, we consider it successful if there's content and no error
if (observation.payload.extras.impl_source === "oh_aci") {
causeMessage.success =
observation.payload.content.length > 0 &&
!observation.payload.content.startsWith("ERROR:\n");
} else {
causeMessage.success =
observation.payload.content.length > 0 &&
!observation.payload.content.toLowerCase().includes("error:");
}
}
if (observationID === "run" || observationID === "run_ipython") {
let { content } = observation.payload;
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
}
content = `${causeMessage.content}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
causeMessage.content = content; // Observation content includes the action
} else if (observationID === "read") {
causeMessage.content = `\`\`\`\n${observation.payload.content}\n\`\`\``; // Content is already truncated by the ACI
} else if (observationID === "edit") {
if (causeMessage.success) {
causeMessage.content = `\`\`\`diff\n${observation.payload.extras.diff}\n\`\`\``; // Content is already truncated by the ACI
} else {
causeMessage.content = observation.payload.content;
}
} else if (observationID === "browse") {
let content = `**URL:** ${observation.payload.extras.url}\n`;
if (observation.payload.extras.error) {
content += `\n\n**Error:**\n${observation.payload.extras.error}\n`;
}
content += `\n\n**Output:**\n${observation.payload.content}`;
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...(truncated)`;
}
causeMessage.content = content;
} else if (observationID === "mcp") {
// For MCP observations, we want to show the content as formatted output
// similar to how run/run_ipython actions are handled
let { content } = observation.payload;
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
}
content = `${causeMessage.content}\n\n**Output:**\n\`\`\`\n${content.trim() || "[MCP Tool finished execution with no output]"}\n\`\`\``;
causeMessage.content = content; // Observation content includes the action
// Set success based on whether there's an error message
causeMessage.success = !observation.payload.content
.toLowerCase()
.includes("error:");
}
},
addErrorMessage(
state: SliceState,
action: PayloadAction<{ id?: string; message: string }>,
) {
const { id, message } = action.payload;
state.messages.push({
translationID: id,
content: message,
type: "error",
sender: "assistant",
timestamp: new Date().toISOString(),
});
},
clearMessages(state: SliceState) {
state.messages = [];
state.systemMessage = null;
},
setMessageFeedback(
state: SliceState,
action: PayloadAction<{
messageId: number;
feedbackType: "positive" | "negative";
}>,
) {
const { messageId, feedbackType } = action.payload;
const messageIndex = state.messages.findIndex(
(message) => message.eventID === messageId,
);
if (messageIndex !== -1) {
state.messages[messageIndex].feedback = feedbackType;
}
},
},
});
export const {
addUserMessage,
addAssistantMessage,
addAssistantAction,
addAssistantObservation,
addErrorMessage,
clearMessages,
setMessageFeedback,
} = chatSlice.actions;
// Selectors
export const selectSystemMessage = (state: { chat: SliceState }) =>
state.chat.systemMessage;
export default chatSlice.reducer;

View File

@@ -42,6 +42,9 @@ enum ActionType {
// Changes the state of the agent, e.g. to paused or running
CHANGE_AGENT_STATE = "change_agent_state",
// User feedback on messages or the entire trajectory
USER_FEEDBACK = "user_feedback",
// Interact with the MCP server.
MCP = "call_tool_mcp",
}

View File

@@ -143,6 +143,18 @@ export interface RejectAction extends OpenHandsActionEvent<"reject"> {
};
}
export interface UserFeedbackAction
extends OpenHandsActionEvent<"user_feedback"> {
source: "user";
args: {
feedback_type: "positive" | "negative";
target_type: "message" | "trajectory";
target_id?: number; // Event ID for message feedback, null for trajectory feedback
rating?: number; // 1-5 rating for SAAS mode
reason?: string | null; // Reason for the rating in SAAS mode
};
}
export interface RecallAction extends OpenHandsActionEvent<"recall"> {
source: "agent";
args: {
@@ -176,5 +188,6 @@ export type OpenHandsAction =
| FileEditAction
| FileWriteAction
| RejectAction
| UserFeedbackAction
| RecallAction
| MCPAction;

View File

@@ -15,6 +15,7 @@ export type OpenHandsEventType =
| "think"
| "finish"
| "error"
| "user_feedback"
| "recall"
| "mcp"
| "call_tool_mcp"

View File

@@ -45,6 +45,8 @@ export type Settings = {
SEARCH_API_KEY?: string;
IS_NEW_USER?: boolean;
MCP_CONFIG?: MCPConfig;
EMAIL?: string;
EMAIL_VERIFIED?: boolean;
};
export type ApiSettings = {
@@ -68,6 +70,8 @@ export type ApiSettings = {
sse_servers: (string | MCPSSEServer)[];
stdio_servers: MCPStdioServer[];
};
email?: string;
email_verified?: boolean;
};
export type PostSettings = Settings & {

View File

@@ -14,5 +14,6 @@ export function loadFeatureFlag(
export const BILLING_SETTINGS = () => loadFeatureFlag("BILLING_SETTINGS");
export const HIDE_LLM_SETTINGS = () => loadFeatureFlag("HIDE_LLM_SETTINGS");
export const VSCODE_IN_NEW_TAB = () => loadFeatureFlag("VSCODE_IN_NEW_TAB");
export const ENABLE_TRAJECTORY_REPLAY = () =>
loadFeatureFlag("TRAJECTORY_REPLAY");

View File

@@ -30,7 +30,7 @@ export const TIPS: Tip[] = [
},
{
key: I18nKey.TIPS$GITHUB_HOOK,
link: "https://docs.all-hands.dev/usage/cloud/cloud-issue-resolver",
link: "https://docs.all-hands.dev/usage/cloud/github-installation#working-on-github-issues-and-pull-requests-using-openhands",
},
{
key: I18nKey.TIPS$BLOG_SIGNUP,

View File

@@ -117,7 +117,10 @@ You can see an example of a repo agent in [the agent for the OpenHands repo itse
- Include repository structure details
- Specify testing and build procedures
- List environment requirements
- Document CI workflows and checks
- Include information about code quality standards
- Maintain up-to-date team practices
- Consider using OpenHands to generate a comprehensive repo.md (see [Creating a Repository Agent](#creating-a-repository-agent))
- YAML frontmatter is optional - files without frontmatter will be loaded with default settings
### Submission Process

View File

@@ -0,0 +1,65 @@
---
name: add_repo_inst
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /add_repo_inst
inputs:
- name: REPO_FOLDER_NAME
description: "Branch for the agent to work on"
---
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
Specifically, I want you to create a `.openhands/microagents/repo.md` file. This file should contain succinct information that summarizes (1) the purpose of this repository, (2) the general setup of this repo, and (3) a brief description of the structure of this repo.
Here's an example:
```markdown
---
name: repo
type: repo
agent: CodeActAgent
---
This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend
(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.
Before pushing any changes, you should ensure that any lint errors or simple test errors have been fixed.
* If you've made changes to the backend, you should run `pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml`
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
If either command fails, it may have automatically fixed some issues. You should fix any issues that weren't automatically fixed,
then re-run the command to ensure it passes.
## 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
- Prerequisites: A recent version of NodeJS / NPM
- Setup: Run `npm install` in the frontend directory
- Testing:
- Run tests: `npm run test`
- To run specific tests: `npm run test -- -t "TestName"`
- Building:
- Build for production: `npm run build`
- Environment Variables:
- Set in `frontend/.env` or as environment variables
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
- Internationalization:
- Generate i18n declaration file: `npm run make-i18n`
```
Now, please write a similar markdown for the current repository.
Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.

View File

@@ -0,0 +1,19 @@
---
name: address_pr_comments
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /address_pr_comments
inputs:
- name: PR_URL
description: "URL of the pull request"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
---
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.
This branch corresponds to this PR {{ PR_URL }}
Next, you should use the GitHub API to read the reviews and comments on this PR and address them.

23
microagents/fix_test.md Normal file
View File

@@ -0,0 +1,23 @@
---
name: fix_test
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /fix_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.

View File

@@ -0,0 +1,21 @@
---
name: update_pr_description
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /update_pr_description
inputs:
- name: PR_URL
description: "URL of the pull request"
type: string
validation:
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
type: string
---
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".
Once you understand the purpose of the diff, please use Github API to read the existing PR description, and update it to be more reflective of the changes we've made when necessary.

View File

@@ -0,0 +1,19 @@
---
name: update_test
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /update_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
The current implementation of the code is correct BUT the test functions {{ FUNCTION_TO_FIX }} in file {{ FILE_FOR_FUNCTION }} are failing.
Please update the test file so that they pass with the current version of the implementation.

View File

@@ -73,7 +73,10 @@ class SandboxConfig(BaseModel):
runtime_startup_env_vars: dict[str, str] = Field(default_factory=dict)
browsergym_eval_env: str | None = Field(default=None)
platform: str | None = Field(default=None)
close_delay: int = Field(default=15)
close_delay: int = Field(
default=3600,
description='The delay in seconds before closing the sandbox after the agent is done.',
)
remote_runtime_resource_factor: int = Field(default=1)
enable_gpu: bool = Field(default=False)
docker_runtime_kwargs: dict | None = Field(default=None)

View File

@@ -91,3 +91,6 @@ class ActionType(str, Enum):
CONDENSATION = 'condensation'
"""Condenses a list of events into a summary."""
USER_FEEDBACK = 'user_feedback'
"""User feedback on messages or the entire trajectory."""

View File

@@ -10,6 +10,7 @@ from openhands.events.action.agent import (
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
from openhands.events.action.commands import CmdRunAction, IPythonRunCellAction
from openhands.events.action.empty import NullAction
from openhands.events.action.feedback import UserFeedbackAction
from openhands.events.action.files import (
FileEditAction,
FileReadAction,
@@ -38,4 +39,5 @@ __all__ = [
'AgentThinkAction',
'RecallAction',
'MCPAction',
'UserFeedbackAction',
]

View File

@@ -0,0 +1,32 @@
from dataclasses import dataclass
from typing import Literal, Optional
from openhands.core.schema import ActionType
from openhands.events.action.action import Action
@dataclass
class UserFeedbackAction(Action):
"""An action where the user provides feedback on a message or the entire trajectory.
Attributes:
feedback_type (str): The type of feedback, either "positive" or "negative".
target_type (str): The target of the feedback, either "message" or "trajectory".
target_id (Optional[int]): The ID of the target message, if target_type is "message".
rating (Optional[int]): A numeric rating from 1-5 for the feedback (used in SAAS mode).
reason (Optional[str]): A reason for the feedback (used in SAAS mode).
action (str): The action type, namely ActionType.USER_FEEDBACK.
"""
feedback_type: Literal["positive", "negative"]
target_type: Literal["message", "trajectory"]
target_id: Optional[int] = None
rating: Optional[int] = None
reason: Optional[str] = None
action: str = ActionType.USER_FEEDBACK
@property
def message(self) -> str:
if self.target_type == "message":
return f"User provided {self.feedback_type} feedback for message {self.target_id}"
return f"User provided {self.feedback_type} feedback for the trajectory"

View File

@@ -3,6 +3,7 @@ from typing import Iterable
from urllib.parse import urlencode
import httpx # type: ignore
from fastapi import status
from openhands.events.event import Event
from openhands.events.event_filter import EventFilter
@@ -42,6 +43,9 @@ class NestedEventStore(EventStoreABC):
if self.session_api_key:
headers['X-Session-API-Key'] = self.session_api_key
response = httpx.get(url, headers=headers)
if response.status_code == status.HTTP_404_NOT_FOUND:
# Follow pattern of event store not throwing errors on not found
return
result_set = response.json()
for result in result_set['events']:
event = event_from_dict(result)

View File

@@ -0,0 +1,19 @@
import os
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.utils.import_utils import get_impl
class ExperimentManager:
@staticmethod
def run_conversation_variant_test(
user_id: str, conversation_id: str, conversation_settings: ConversationInitData
) -> ConversationInitData:
return conversation_settings
experiment_manager_cls = os.environ.get(
'OPENHANDS_EXPERIMENT_MANAGER_CLS',
'openhands.experiments.experiment_manager.ExperimentManager',
)
ExperimentManagerImpl = get_impl(ExperimentManager, experiment_manager_cls)

View File

@@ -483,35 +483,30 @@ class GitHubService(BaseGitService, GitService):
- PR URL when successful
- Error message when unsuccessful
"""
try:
url = f'{self.BASE_URL}/repos/{repo_name}/pulls'
# Set default body if none provided
if not body:
body = f'Merging changes from {source_branch} into {target_branch}'
url = f'{self.BASE_URL}/repos/{repo_name}/pulls'
# Prepare the request payload
payload = {
'title': title,
'head': source_branch,
'base': target_branch,
'body': body,
'draft': draft,
}
# Set default body if none provided
if not body:
body = f'Merging changes from {source_branch} into {target_branch}'
# Make the POST request to create the PR
response, _ = await self._make_request(
url=url, params=payload, method=RequestMethod.POST
)
# Prepare the request payload
payload = {
'title': title,
'head': source_branch,
'base': target_branch,
'body': body,
'draft': draft,
}
# Return the HTML URL of the created PR
if 'html_url' in response:
return response['html_url']
else:
return f'PR created but URL not found in response: {response}'
# Make the POST request to create the PR
response, _ = await self._make_request(
url=url, params=payload, method=RequestMethod.POST
)
# Return the HTML URL of the created PR
return response['html_url']
except Exception as e:
return f'Error creating pull request: {str(e)}'
github_service_cls = os.environ.get(

View File

@@ -476,38 +476,33 @@ class GitLabService(BaseGitService, GitService):
- MR URL when successful
- Error message when unsuccessful
"""
try:
# Convert string ID to URL-encoded path if needed
project_id = str(id).replace('/', '%2F') if isinstance(id, str) else id
url = f'{self.BASE_URL}/projects/{project_id}/merge_requests'
# Set default description if none provided
if not description:
description = (
f'Merging changes from {source_branch} into {target_branch}'
)
# Convert string ID to URL-encoded path if needed
project_id = str(id).replace('/', '%2F') if isinstance(id, str) else id
url = f'{self.BASE_URL}/projects/{project_id}/merge_requests'
# Prepare the request payload
payload = {
'source_branch': source_branch,
'target_branch': target_branch,
'title': title,
'description': description,
}
# Make the POST request to create the MR
response, _ = await self._make_request(
url=url, params=payload, method=RequestMethod.POST
# Set default description if none provided
if not description:
description = (
f'Merging changes from {source_branch} into {target_branch}'
)
# Return the web URL of the created MR
if 'web_url' in response:
return response['web_url']
else:
return f'MR created but URL not found in response: {response}'
# Prepare the request payload
payload = {
'source_branch': source_branch,
'target_branch': target_branch,
'title': title,
'description': description,
}
# Make the POST request to create the MR
response, _ = await self._make_request(
url=url, params=payload, method=RequestMethod.POST
)
return response['web_url']
except Exception as e:
return f'Error creating merge request: {str(e)}'
gitlab_service_cls = os.environ.get(

View File

@@ -167,11 +167,11 @@ class BaseGitService(ABC):
return RateLimitError('GitHub API rate limit exceeded')
logger.warning(f'Status error on {self.provider} API: {e}')
return UnknownException('Unknown error')
return UnknownException(f'Unknown error: {e}')
def handle_http_error(self, e: HTTPError) -> UnknownException:
logger.warning(f'HTTP error on {self.provider} API: {type(e).__name__} : {e}')
return UnknownException(f'HTTP error {type(e).__name__}')
return UnknownException(f'HTTP error {type(e).__name__} : {e}')
class GitService(Protocol):

View File

@@ -15,4 +15,3 @@ When you're done, make sure to
2. Use the `create_pr` tool to open a new PR
3. Name the branch using `openhands/` as a prefix (e.g `openhands/update-readme`)
4. The PR description should mention that it "fixes" or "closes" the issue number
5. Make sure to leave the following sentence at the end of the PR description: `@{{ username }} can click here to [continue refining the PR]({{ conversation_url }})`

View File

@@ -9,4 +9,3 @@ When you're done, make sure to
1. Use the `create_pr` tool to open a new PR
2. The PR description should mention that it "fixes" or "closes" the issue number
3. Make sure to leave the following sentence at the end of the PR description: `@{{ username }} can click here to [continue refining the PR]({{ conversation_url }})`

View File

@@ -15,4 +15,3 @@ When you're done, make sure to
2. Use the `create_mr` tool to open a new MR
3. Name the branch using `openhands/` as a prefix (e.g `openhands/update-readme`)
4. The MR description should mention that it "fixes" or "closes" the issue number
5. Make sure to leave the following sentence at the end of the MR description: `@{{ username }} can click here to [continue refining the MR]({{ conversation_url }})`

View File

@@ -9,4 +9,3 @@ When you're done, make sure to
1. Use the `create_mr` tool to open a new MR
2. The MR description should mention that it "fixes" or "closes" the issue number
3. Make sure to leave the following sentence at the end of the MR description: `@{{ username }} can click here to [continue refining the MR]({{ conversation_url }})`

View File

@@ -5,7 +5,3 @@ These are a list of text messages attached in order of most recent.
{{ message }}
{% if not loop.last %}\n\n{% endif %}
{% endfor %}
If you opened a pull request, please leave the following comment at the end your summary and pull request description
`{{ username }} can click here to [continue refining the PR]({{ conversation_url }})`

Some files were not shown because too many files have changed in this diff Show More