Compare commits

...

61 Commits

Author SHA1 Message Date
openhands 1ff269a073 Add note about using /tmp for temporary files 2025-06-13 19:58:35 +00:00
Ray Myers e6036b8346 Bump version for 0.43.0 release (#9109) 2025-06-13 14:47:26 -05:00
jpelletier1 144d09a578 Code review microagent (#9093)
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-13 01:35:44 +00:00
llamantino f97a837d46 fix: fix unreachable runtime container in make docker-dev (#9072) 2025-06-12 12:46:10 -04:00
dependabot[bot] eadec4ce9e chore(deps): bump the version-all group in /frontend with 8 updates (#9095)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 15:17:45 +00:00
dependabot[bot] 49e8737779 chore(deps): bump the version-all group across 1 directory with 24 updates (#9066)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-06-12 14:31:35 +00:00
Graham Neubig 4711e74101 Fix default provider in CLI to be 'anthropic' instead of 'openai' (#9004)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-12 03:02:03 +00:00
mamoodi c87f1cc8c0 Move Advanced Configurations under Running OpenHands on your Own (#9082) 2025-06-11 16:36:17 -04:00
Rohit Malhotra 33b64786b0 [Docs]: add info about lower scope tokens for gitlab (#9017)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-06-11 19:34:06 +00:00
Rohit Malhotra 12fc50299b [Docs]: add slack integration docs (#8903)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-11 19:32:54 +00:00
Tim O'Farrell 57fee17348 Fix VSCode workspace dir (#9080) 2025-06-11 13:31:59 -06:00
Engel Nyst 77517d8ba0 Save CLI settings directly under ~/.openhands (#9079) 2025-06-11 21:07:40 +02:00
Calvin Smith a356f56237 fix: Context window truncation makes progress (#9052)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-11 12:47:34 -06:00
chuckbutkus 7dede37fd8 Make sure redirect URI is HTTPS unless it is for localhost (#9076) 2025-06-11 18:19:15 +00:00
Ray Myers c11dcad309 Add more log context on key events (#9056) 2025-06-11 11:34:16 -05:00
Tim O'Farrell 47209e794a Runtime Status Fixes (#9050)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-11 09:28:17 -06:00
Xingyao Wang 3f50eb0079 feat: Add microagents UI to conversation context menu (#8984)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-06-11 23:12:27 +08:00
sp.wack f27b02411b chore: Add deprecated tag to ActionMessage type (#9063) 2025-06-11 18:34:07 +04:00
llamantino d151093872 docs: added devstral to llms list, added local llms in local setup (#9062)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-06-11 10:22:15 -04:00
neo ea7294b7f9 docs: add links to other language versions of README (#9038)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-06-11 09:49:40 -04:00
Xingyao Wang 9097f487a6 Move get_agent_obs_text function to browser utils and add return_all option (#9019)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-11 12:32:38 +08:00
Rohit Malhotra fd921a4f88 [Fix]: model tracking in convo metadata (#9053) 2025-06-10 22:19:33 -04:00
Xingyao Wang 96fe5a50d6 Update repo.md (#9054) 2025-06-10 21:51:13 -04:00
Howie Zhou b634e10b45 Add JSON serialization for array and object parameters when converting tools (#8780) 2025-06-10 16:48:49 -04:00
Xingyao Wang 73f01657eb docs: Add TanStack Query state management documentation (#9047) 2025-06-10 16:44:00 -04:00
mamoodi 5d328183d5 Release 0.42.0 (#9046) 2025-06-10 16:34:10 -04:00
Mislav Lukach b7da65d373 chore(ui): update tailwind (#9049)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-06-10 18:20:04 +00:00
sp.wack dca9c7bdc6 feat(backend): New "update microagent prompt" API (#8357)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-06-10 22:10:55 +04:00
Rene Leonhardt 07862c32cb chore(docker): update docker base images (#8796)
Co-authored-by: Xingyao Wang <xingyaoww@gmail.com>
2025-06-10 22:48:46 +08:00
Emmanuel Ferdman e04f876df9 Migrate to modern logger interface in server utils (#8965)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-06-10 10:25:06 -04:00
Mislav Lukach 78d707de83 chore(billing): add stripe powered by (#9016)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-06-10 18:10:09 +04:00
sp.wack 058153292f fix(ui): startup message ui (#9007) 2025-06-10 16:50:18 +04:00
dependabot[bot] 53b5e08804 chore(deps): bump the version-all group across 1 directory with 15 updates (#9027)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-09 18:04:14 -04:00
Ray Myers 7cee7dca64 chore - log size of large events (#9024) 2025-06-09 16:47:40 -05:00
mamoodi e12a62d006 Update GUI docs (#9020) 2025-06-09 15:38:44 -04:00
llamantino 77a0c5e073 feat: increase requests timeout to 60s (#8974)
Co-authored-by: llamantino <12345678+yourusername@users.noreply.github.com>
2025-06-09 12:42:03 -06:00
Tim O'Farrell e5d21e003d Added environment variable allowing skipping dependency checks (#9010) 2025-06-09 11:14:39 -06:00
mamoodi c6a4324bda Update Cloud API docs (#9008) 2025-06-09 11:42:37 -04:00
Tim O'Farrell 9ac8f011fe Converted exponential backoff to fixed (#9006) 2025-06-09 09:02:52 -06:00
Leander Maben d84befe28f Adding LLM Based Editing capability (#8677)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-06-09 21:57:20 +08:00
mamoodi 4eef22e04e Fix some broken links (#9005) 2025-06-09 13:37:00 +00:00
Graham Neubig 93e6811efc Add CLI option to bug template installation dropdown (#9002)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-09 08:49:47 -04:00
Graham Neubig 3ebe3c2140 Update CLI mode documentation to recommend pip install (#8967)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-09 08:13:09 -04: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
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
219 changed files with 8691 additions and 4850 deletions
+1
View File
@@ -12,4 +12,5 @@
"ghcr.io/devcontainers/features/node:1": {},
},
"postCreateCommand": ".devcontainer/setup.sh",
"runArgs": ["--network=host"],
}
Regular → Executable
View File
+22 -4
View File
@@ -1,5 +1,23 @@
# NodeJS
frontend/node_modules
config.toml
.envrc
.env
.git
# Configuration (except pyproject.toml)
*.ini
*.toml
!pyproject.toml
*.yml
# Documentation (except README.md)
*.md
!README.md
# Hidden files and directories
.*
__pycache__
# Unneded files and directories
/dev_config/
/docs/
/evaluation/
/tests/
CITATION.cff
+1 -1
View File
@@ -5,7 +5,7 @@
/frontend/ @rbren @amanape
# Evaluation code owners
/evaluation/ @xingyaoww @neubig
/evaluation/ @xingyaoww @neubig
# Documentation code owners
/docs/ @mamoodi
+1
View File
@@ -33,6 +33,7 @@ body:
- Docker command in README
- GitHub resolver
- Development workflow
- CLI
- app.all-hands.dev
- Other
default: 0
+6 -1
View File
@@ -16,7 +16,6 @@ updates:
mcp-packages:
patterns:
- "mcp"
- "mcpm"
security-all:
applies-to: "security-updates"
patterns:
@@ -73,3 +72,9 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directories:
- "containers/*"
schedule:
interval: "weekly"
+7 -1
View File
@@ -44,7 +44,13 @@ Frontend:
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
- Internationalization:
- Generate i18n declaration file: `npm run make-i18n`
- Data Fetching & Cache Management:
- We use TanStack Query (fka React Query) for data fetching and cache management
- Data Access Layer: API client methods are located in `frontend/src/api` and should never be called directly from UI components - they must always be wrapped with TanStack Query
- Custom hooks are located in `frontend/src/hooks/query/` and `frontend/src/hooks/mutation/`
- Query hooks should follow the pattern use[Resource] (e.g., `useConversationMicroagents`)
- Mutation hooks should follow the pattern use[Action] (e.g., `useDeleteConversation`)
- Architecture rule: UI components → TanStack Query hooks → Data Access Layer (`frontend/src/api`) → API endpoints
## Template for Github Pull Request
+1 -1
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.41-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.43-nikolaik`
## Develop inside Docker container
+14 -3
View File
@@ -18,6 +18,17 @@
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
<!-- Keep these links. Translations will automatically update with the README. -->
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=de">Deutsch</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=es">Español</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=fr">français</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ja">日本語</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ko">한국어</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=pt">Português</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ru">Русский</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=zh">中文</a>
<hr>
</div>
@@ -51,17 +62,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.41
docker.all-hands.dev/all-hands-ai/openhands:0.43
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+3 -3
View File
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.41
docker.all-hands.dev/all-hands-ai/openhands:0.43
```
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands
+11 -15
View File
@@ -1,16 +1,16 @@
ARG OPENHANDS_BUILD_VERSION=dev
FROM node:21.7.2-bookworm-slim AS frontend-builder
FROM node:22.16.0-bookworm-slim AS frontend-builder
WORKDIR /app
COPY ./frontend/package.json frontend/package-lock.json ./
RUN npm install -g npm@10.5.1
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY ./frontend ./
COPY frontend ./
RUN npm run build
FROM python:3.12.3-slim AS backend-builder
FROM python:3.12.10-slim AS base
FROM base AS backend-builder
WORKDIR /app
ENV PYTHONPATH='/app'
@@ -22,17 +22,18 @@ ENV POETRY_NO_INTERACTION=1 \
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential \
&& python3 -m pip install poetry==1.8.2 --break-system-packages
&& python3 -m pip install poetry --break-system-packages
COPY ./pyproject.toml ./poetry.lock ./
COPY pyproject.toml poetry.lock ./
RUN touch README.md
RUN export POETRY_CACHE_DIR && poetry install --no-root && rm -rf $POETRY_CACHE_DIR
FROM python:3.12.3-slim AS openhands-app
FROM base AS openhands-app
WORKDIR /app
ARG OPENHANDS_BUILD_VERSION #re-declare for this section
# re-declare for this section
ARG OPENHANDS_BUILD_VERSION
ENV RUN_AS_OPENHANDS=true
# A random number--we need this to be different from the user's UID on the host machine
@@ -74,12 +75,7 @@ COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${V
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:app --chmod=770 ./openhands/agenthub ./openhands/agenthub
COPY --chown=openhands:app ./pyproject.toml ./pyproject.toml
COPY --chown=openhands:app ./poetry.lock ./poetry.lock
COPY --chown=openhands:app ./README.md ./README.md
COPY --chown=openhands:app ./MANIFEST.in ./MANIFEST.in
COPY --chown=openhands:app ./LICENSE ./LICENSE
COPY --chown=openhands:app pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership
RUN python openhands/core/download.py # No-op to download assets
+2 -1
View File
@@ -11,11 +11,12 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.41-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.43-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
- "3000:3000"
network_mode: host
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
+1 -1
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.41-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+17
View File
@@ -0,0 +1,17 @@
# Setup
```
npm install -g mint
```
or
```
yarn global add mint
```
# Preview
```
mint dev
```
+56 -51
View File
@@ -34,28 +34,77 @@
"group": "Integrations",
"pages": [
"usage/cloud/github-installation",
"usage/cloud/gitlab-installation"
"usage/cloud/gitlab-installation",
"usage/cloud/slack-installation"
]
},
"usage/cloud/cloud-ui",
"usage/cloud/cloud-issue-resolver",
"usage/cloud/cloud-api"
]
},
{
"group": "Running OpenHands Locally",
"group": "Running OpenHands on Your Own",
"pages": [
"usage/local-setup",
"usage/how-to/gui-mode",
"usage/how-to/cli-mode",
"usage/how-to/headless-mode",
"usage/how-to/github-action"
"usage/how-to/github-action",
{
"group": "Advanced Configuration",
"pages": [
{
"group": "LLM Configuration",
"pages": [
"usage/llms/llms",
{
"group": "Providers",
"pages": [
"usage/llms/azure-llms",
"usage/llms/google-llms",
"usage/llms/groq",
"usage/llms/local-llms",
"usage/llms/litellm-proxy",
"usage/llms/openai-llms",
"usage/llms/openrouter"
]
}
]
},
{
"group": "Runtime Configuration",
"pages": [
"usage/runtimes/overview",
{
"group": "Providers",
"pages": [
"usage/runtimes/docker",
"usage/runtimes/remote",
"usage/runtimes/local",
{
"group": "Third-Party Providers",
"pages": [
"usage/runtimes/modal",
"usage/runtimes/daytona",
"usage/runtimes/runloop",
"usage/runtimes/e2b"
]
}
]
}
]
},
"usage/configuration-options",
"usage/how-to/custom-sandbox-guide",
"usage/search-engine-setup",
"usage/mcp"
]
}
]
},
{
"group": "Customization",
"pages": [
"usage/prompting/prompting-best-practices",
"usage/prompting/repository",
{
"group": "Microagents",
@@ -70,53 +119,9 @@
]
},
{
"group": "Advanced Configuration",
"group": "Tips and Tricks",
"pages": [
{
"group": "LLM Configuration",
"pages": [
"usage/llms/llms",
{
"group": "Providers",
"pages": [
"usage/llms/azure-llms",
"usage/llms/google-llms",
"usage/llms/groq",
"usage/llms/local-llms",
"usage/llms/litellm-proxy",
"usage/llms/openai-llms",
"usage/llms/openrouter"
]
}
]
},
{
"group": "Runtime Configuration",
"pages": [
"usage/runtimes/overview",
{
"group": "Providers",
"pages": [
"usage/runtimes/docker",
"usage/runtimes/remote",
"usage/runtimes/local",
{
"group": "Third-Party Providers",
"pages": [
"usage/runtimes/modal",
"usage/runtimes/daytona",
"usage/runtimes/runloop",
"usage/runtimes/e2b"
]
}
]
}
]
},
"usage/configuration-options",
"usage/how-to/custom-sandbox-guide",
"usage/search-engine-setup",
"usage/mcp"
"usage/prompting/prompting-best-practices"
]
},
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

+79 -85
View File
@@ -1,9 +1,11 @@
---
title: Cloud API
description: OpenHands Cloud provides a REST API that allows you to programmatically interact with the service. This guide explains how to obtain an API key and use the API to start conversations.
description: OpenHands Cloud provides a REST API that allows you to programmatically interact with OpenHands.
This guide explains how to obtain an API key and use the API to start conversations and retrieve their status.
---
For more detailed information about the API, refer to the [OpenHands API Reference](https://docs.all-hands.dev/swagger-ui/).
For the available API endpoints, refer to the
[OpenHands API Reference](https://docs.all-hands.dev/api-reference).
## Obtaining an API Key
@@ -16,7 +18,7 @@ To use the OpenHands Cloud API, you'll need to generate an API key:
5. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
6. Copy the generated API key and store it securely. It will only be shown once.
![API Key Generation](/static/img/docs/api-key-generation.png)
![API Key Generation](/static/img/api-key-generation.png)
## API Usage
@@ -33,87 +35,81 @@ To start a new conversation with OpenHands to perform a task, you'll need to mak
#### Examples
<details>
<summary>cURL</summary>
```bash
curl -X POST "https://app.all-hands.dev/api/conversations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}'
```
</details>
<Accordion title="cURL">
```bash
curl -X POST "https://app.all-hands.dev/api/conversations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}'
```
</Accordion>
<details>
<summary>Python (with requests)</summary>
<Accordion title="Python (with requests)">
```python
import requests
```python
import requests
api_key = "YOUR_API_KEY"
url = "https://app.all-hands.dev/api/conversations"
api_key = "YOUR_API_KEY"
url = "https://app.all-hands.dev/api/conversations"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
data = {
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}
response = requests.post(url, headers=headers, json=data)
conversation = response.json()
print(f"Conversation Link: https://app.all-hands.dev/conversations/{conversation['conversation_id']}")
print(f"Status: {conversation['status']}")
```
</details>
<details>
<summary>TypeScript/JavaScript (with fetch)</summary>
```typescript
const apiKey = "YOUR_API_KEY";
const url = "https://app.all-hands.dev/api/conversations";
const headers = {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
};
const data = {
initial_user_msg: "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
repository: "yourusername/your-repo"
};
async function startConversation() {
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(data)
});
const conversation = await response.json();
console.log(`Conversation Link: https://app.all-hands.dev/conversations/${conversation.id}`);
console.log(`Status: ${conversation.status}`);
return conversation;
} catch (error) {
console.error("Error starting conversation:", error);
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
}
startConversation();
```
data = {
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}
</details>
response = requests.post(url, headers=headers, json=data)
conversation = response.json()
print(f"Conversation Link: https://app.all-hands.dev/conversations/{conversation['conversation_id']}")
print(f"Status: {conversation['status']}")
```
</Accordion>
<Accordion title="TypeScript/JavaScript (with fetch)">
```typescript
const apiKey = "YOUR_API_KEY";
const url = "https://app.all-hands.dev/api/conversations";
const headers = {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
};
const data = {
initial_user_msg: "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
repository: "yourusername/your-repo"
};
async function startConversation() {
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(data)
});
const conversation = await response.json();
console.log(`Conversation Link: https://app.all-hands.dev/conversations/${conversation.id}`);
console.log(`Status: ${conversation.status}`);
return conversation;
} catch (error) {
console.error("Error starting conversation:", error);
}
}
startConversation();
```
</Accordion>
#### Response
@@ -145,14 +141,12 @@ GET https://app.all-hands.dev/api/conversations/{conversation_id}
#### Example
<details>
<summary>cURL</summary>
```bash
curl -X GET "https://app.all-hands.dev/api/conversations/{conversation_id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
</details>
<Accordion title="cURL">
```bash
curl -X GET "https://app.all-hands.dev/api/conversations/{conversation_id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
</Accordion>
#### Response
-33
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
+23 -15
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](/usage/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.
+5 -5
View File
@@ -1,7 +1,7 @@
---
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 issues!
set up, it will allow OpenHands to work with your GitHub repository through the Cloud UI or straight from GitHub!
---
## Prerequisites
@@ -37,11 +37,11 @@ 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
## Working With Github Repos in Openhands Cloud
## Working With GitHub Repos in Openhands Cloud
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 session!
click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
@@ -67,5 +67,5 @@ To get OpenHands to work on pull requests, mention `@openhands` in the comments
## 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.
+18 -10
View File
@@ -1,23 +1,31 @@
---
title: GitLab Integration
description: This guide walks you through the process of installing and configuring OpenHands Cloud for your GitLab repositories.
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)
## Using Tokens with Reduced Scopes
OpenHands requests an API-scoped token during OAuth authentication. By default, this token is provided to the agent.
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default token assigned to the agent.
While the high-permission API token is still requested and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
## Next Steps
- [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.
+5 -5
View File
@@ -14,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.
+52
View File
@@ -0,0 +1,52 @@
---
title: Slack Integration - Coming soon...
description: This guide walks you through installing the OpenHands Slack app.
---
<Warning>This integration is not live yet, but will be available soon.</Warning>
## Prerequisites
- You are a slack workspace admin
- Access to OpenHands Cloud
## Installation Steps
1. Log in to [OpenHands Cloud](https://app.all-hands.dev)
2. Click the button below to OpenHands Slack App <a target="_blank" href="https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,chat:write,users:read,channels:history,groups:history,mpim:history,im:history&user_scope=channels:history,groups:history,im:history,mpim:history"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
3. In the top right corner, select the workspace to install the OpenHands Slack app.
4. Review permissions and click allow
## Working With the Slack App
To start a new conversation, you can mention `@openhands` in a new message or a thread inside any Slack channel.
Once a conversation is started, all thread messages underneath it will be follow-up messages to OpenHands.
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message. You must be the user who started the conversation.
## Example conversation
### Start a new conversation, and select repo
Conversation is started by mentioning `@openhands`.
![slack-create-convo.png](/static/img/slack-create-convo.png)
### See agent response and send follow up messages
Initial request is followed up by mentioning `@openhands` in a thread reply.
![slack-results-and-follow-up.png](/static/img/slack-results-and-follow-up.png)
## Pro tip
You can mention a repo name when starting a new conversation in the following formats
1. "My-Repo" repo (e.g `@openhands in the openhands repo ...`)
2. "All-Hands-AI/OpenHands" (e.g `@openhands in All-Hands-AI/OpenHands ...`)
The repo match is case insensitive. If a repo name match is made, it will kick off the conversation.
If the repo name partially matches against, multiple repos, you'll be asked to select a repo from the filtered list.
![slack-pro-tip.png](/static/img/slack-pro-tip.png)
+22 -7
View File
@@ -1,24 +1,39 @@
---
title: CLI Mode
description: CLI mode provides a powerful interactive Command-Line Interface (CLI) that lets you engage with OpenHands directly from your terminal.
title: CLI
description: The Command-Line Interface (CLI) provides a powerful interface that lets you engage with OpenHands
directly from your terminal.
---
This mode is different from the [headless mode](./headless-mode), which is non-interactive and better for scripting.
This mode is different from the [headless mode](/usage/how-to/headless-mode), which is non-interactive and better
for scripting.
## Getting Started
### Running with Python
1. Ensure you have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
1. Install OpenHands using pip:
```bash
pip install openhands-ai
```
2. Set your model, API key, and other preferences using environment variables or with the [`config.toml`](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) file.
3. Launch an interactive OpenHands conversation from the command line:
```bash
poetry run python -m openhands.cli.main
openhands
```
This command opens an interactive prompt where you can type tasks or commands and get responses from OpenHands.
#### For Developers
If you have cloned the repository, you can run the CLI directly using Poetry:
```bash
poetry run python -m openhands.cli.main
```
### Running with Docker
1. Set the following environment variables in your terminal:
@@ -31,7 +46,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.41-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -40,7 +55,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
docker.all-hands.dev/all-hands-ai/openhands:0.43 \
python -m openhands.cli.main --override-cli-mode true
```
+1 -1
View File
@@ -46,7 +46,7 @@ This will produce a new image called `custom-image`, which will be available in
## Using the Docker Command
When running OpenHands using [the docker command](/usage/installation#start-the-app), replace
When running OpenHands using [the docker command](/usage/local-setup#start-the-app), replace
`-e SANDBOX_RUNTIME_CONTAINER_IMAGE=...` with `-e SANDBOX_BASE_CONTAINER_IMAGE=<custom image name>`:
```commandline
+1 -1
View File
@@ -48,6 +48,6 @@ The customization options you can set are:
| `LLM_MODEL` | Variable | Set the LLM to use with OpenHands | `LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"` |
| `OPENHANDS_MAX_ITER` | Variable | Set max limit for agent iterations | `OPENHANDS_MAX_ITER=10` |
| `OPENHANDS_MACRO` | Variable | Customize default macro for invoking the resolver | `OPENHANDS_MACRO=@resolveit` |
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](https://docs.all-hands.dev/modules/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
| `TARGET_BRANCH` | Variable | Merge to branch other than `main` | `TARGET_BRANCH="dev"` |
| `TARGET_RUNNER` | Variable | Target runner to execute the agent workflow (default ubuntu-latest) | `TARGET_RUNNER="custom-runner"` |
+64 -45
View File
@@ -1,14 +1,13 @@
---
title: GUI Mode
description: OpenHands provides a Graphical User Interface (GUI) mode for interacting with the AI assistant.
title: GUI
description: High level overview of the Graphical User Interface (GUI) in OpenHands.
---
## Installation and Setup
## Prerequisites
1. Follow the installation instructions to install OpenHands.
2. After running the command, access OpenHands at [http://localhost:3000](http://localhost:3000).
- [OpenHands is running](/usage/local-setup)
## Interacting with the GUI
## Overview
### Initial Setup
@@ -19,16 +18,23 @@ description: OpenHands provides a Graphical User Interface (GUI) mode for intera
3. Enter the corresponding `API Key` for your chosen provider.
4. Click `Save Changes` to apply the settings.
### Version Control Tokens
### Settings
OpenHands supports multiple version control providers. You can configure tokens for multiple providers simultaneously.
You can use the Settings page at any time to:
#### GitHub Token Setup
- Setup the LLM provider and model for OpenHands.
- [Setup the search engine](/usage/search-engine-setup).
- [Configure MCP servers](/usage/mcp).
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup) and [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
- Set application settings like your preferred language, notifications and other preferences.
- Generate custom secrets.
#### GitHub Setup
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if provided:
<details>
<summary>Setting Up a GitHub Token</summary>
<AccordionGroup>
<Accordion title="Setting Up a GitHub Token">
1. **Generate a Personal Access Token (PAT)**:
- On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
@@ -37,16 +43,11 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if pro
- `repo` (Full control of private repositories)
- **Fine-Grained Tokens**
- All Repositories (You can select specific repositories, but this will impact what returns in repo search)
- Minimal Permissions ( Select `Meta Data = Read-only` read for search, `Pull Requests = Read and Write` and `Content = Read and Write` for branch creation)
- Minimal Permissions (Select `Meta Data = Read-only` read for search, `Pull Requests = Read and Write` and `Content = Read and Write` for branch creation)
2. **Enter Token in OpenHands**:
- Click the Settings button (gear icon).
- Navigate to the `Git` tab.
- In the Settings page, navigate to the `Git` tab.
- Paste your token in the `GitHub Token` field.
- Click `Save Changes` to apply the changes.
</details>
<details>
<summary>Organizational Token Policies</summary>
If you're working with organizational repositories, additional setup may be required:
@@ -59,15 +60,12 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if pro
- Look for the organization under `Organization access`.
- If required, click `Enable SSO` next to your organization.
- Complete the SSO authorization process.
</details>
<details>
<summary>Troubleshooting</summary>
</Accordion>
<Accordion title="Troubleshooting">
Common issues and solutions:
- **Token Not Recognized**:
- Ensure the token is properly saved in settings.
- Check that the token hasn't expired.
- Verify the token has the required scopes.
- Try regenerating the token.
@@ -81,15 +79,15 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if pro
- The app will show a green checkmark if the token is valid.
- Try accessing a repository to confirm permissions.
- Check the browser console for any error messages.
</details>
</Accordion>
</AccordionGroup>
#### GitLab Token Setup
#### GitLab Setup
OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if provided:
<details>
<summary>Setting Up a GitLab Token</summary>
<AccordionGroup>
<Accordion title="Setting Up a GitLab Token">
1. **Generate a Personal Access Token (PAT)**:
- On GitLab, go to User Settings > Access Tokens.
- Create a new token with the following scopes:
@@ -99,15 +97,17 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
- `write_repository` (Write repository)
- Set an expiration date or leave it blank for a non-expiring token.
2. **Enter Token in OpenHands**:
- Click the Settings button (gear icon).
- Navigate to the `Git` tab.
- In the Settings page, navigate to the `Git` tab.
- Paste your token in the `GitLab Token` field.
- Click `Save Changes` to apply the changes.
</details>
<details>
<summary>Troubleshooting</summary>
3. **(Optional): Restrict agent permissions**
- Create another PAT using Step 1 and exclude `api` scope .
- In the Settings page, in the `Secrets` tab, create a new secret `GITLAB_TOKEN` and paste your lower scope token.
- OpenHands will use the higher scope token, and the agent will use the lower scope token
</Accordion>
<Accordion title="Troubleshooting">
Common issues and solutions:
- **Token Not Recognized**:
@@ -119,25 +119,44 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
- Verify project access permissions.
- Check if the token has the necessary scopes.
- For group/organization repositories, ensure you have proper access.
</details>
</Accordion>
</AccordionGroup>
### Advanced Settings
#### Advanced Settings
1. Inside the Settings page, under the `LLM` tab, toggle `Advanced` options to access additional settings.
2. Use the `Custom Model` text box to manually enter a model if it's not in the list.
3. Specify a `Base URL` if required by your LLM provider.
The `Advanced` settings allows configuration of additional LLM settings. Inside the Settings page, under the `LLM` tab,
toggle `Advanced` options to access additional settings.
### Interacting with the AI
- Custom Model: Use the `Custom Model` text box to manually enter a model. Make sure to use the correct prefix based on litellm docs.
- Base URL: Specify a `Base URL` if required by your LLM provider.
- Memory Condensation: The memory condenser manages the LLM's context by ensuring only the most important and relevant information is presented.
- Confirmation Mode: Enabling this mode will cause OpenHands to confirm an action with the user before performing it.
1. Type your prompt in the input box.
2. Click the send button or press Enter to submit your message.
3. The AI will process your input and provide a response in the chat window.
4. You can continue the conversation by asking follow-up questions or providing additional information.
### Key Features
For an overview of the key features available inside a conversation, please refer to the [Key Features](/usage/key-features)
section of the documentation.
### Status Indicator
The status indicator located in the bottom left of the screen will cycle through a number of states as a new conversation
is loaded. Typically these include:
* `Disconnected` : The frontend is not connected to any conversation
* `Connecting` : The frontend is connecting a websocket to a conversation.
* `Building Runtime...` : The server is building a runtime. This is typically in development mode only while building a docker image.
* `Starting Runtime...` : The server is starting a new runtime instance - probably a new docker container or remote runtime.
* `Initializing Agent...` : The server is starting the agent loop. (This step does not appear at present with Nested runtimes)
* `Setting up workspace...` : Usually this means a `git clone ...` operation.
* `Setting up git hooks` : Setting up the git pre commit hooks for the workspace.
* `Agent is awaiting user input...` : Ready to go!
## Tips for Effective Use
- Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
- Use one of the recommended models, as described in the [LLMs section](usage/llms/llms.md).
Remember, the GUI mode of OpenHands is designed to make your interaction with the AI assistant as smooth and intuitive
as possible. Don't hesitate to explore its features to maximize your productivity.
## Other Ways to Run Openhands
- [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 GitHub issues with a GitHub action.](/usage/how-to/github-action)
+6 -5
View File
@@ -1,9 +1,10 @@
---
title: Headless Mode
description: You can run OpenHands with a single command, without starting the web application. This makes it easy to write scripts and automate tasks with OpenHands.
title: Headless
description: You can run OpenHands with a single command, without starting the web application. This makes it easy to
write scripts and automate tasks with OpenHands.
---
This is different from [CLI Mode](./cli-mode), which is interactive, and better for active development.
This is different from [the CLI](./cli-mode), which is interactive, and better for active development.
## With Python
@@ -31,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -41,7 +42,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
docker.all-hands.dev/all-hands-ai/openhands:0.43 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+2 -2
View File
@@ -12,8 +12,8 @@ To get started with OpenHands Cloud, visit [app.all-hands.dev](https://app.all-h
For more information see [getting started with OpenHands Cloud.](/usage/cloud/openhands-cloud)
## Running OpenHands Locally
## Running OpenHands on Your Own
Run OpenHands on your local system and bring your own LLM and API key.
For more information see [running OpenHands locally.](/usage/local-setup)
For more information see [running OpenHands on your own.](/usage/local-setup)
+10 -5
View File
@@ -14,23 +14,28 @@ recommendations for model selection. Our latest benchmarking results can be foun
Based on these findings and community feedback, these are the latest models that have been verified to work reasonably well with OpenHands:
### Cloud / API-Based Models
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) -- available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
to help others using the same provider!
For a full list of the providers and models available, please consult the
[litellm documentation](https://docs.litellm.ai/docs/providers).
<Warning>
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
limits and monitor usage.
</Warning>
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
to help others using the same provider!
### Local / Self-Hosted Models
For a full list of the providers and models available, please consult the
[litellm documentation](https://docs.litellm.ai/docs/providers).
- [mistralai/devstral-small](https://www.all-hands.dev/blog/devstral-a-new-state-of-the-art-open-model-for-coding-agents) (20 May 2025) -- also available through [OpenRouter](https://openrouter.ai/mistralai/devstral-small:free)
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) (31 March 2025) -- also available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
<Note>
Most current local and open source models are not as powerful. When using such models, you may see long
+5 -5
View File
@@ -48,31 +48,31 @@ We recommend using [LMStudio](https://lmstudio.ai/) for serving these models loc
### Start OpenHands with locally served model
Check [the installation guide](https://docs.all-hands.dev/modules/usage/installation) to make sure you have all the prerequisites for running OpenHands.
Check [the installation guide](/usage/local-setup) to make sure you have all the prerequisites for running OpenHands.
```bash
export LMSTUDIO_MODEL_NAME="imported-models/uncategorized/devstralq4_k_m.gguf" # <- Replace this with the model name you copied from LMStudio
export LMSTUDIO_URL="http://host.docker.internal:1234" # <- Replace this with the port from LMStudio
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
mkdir -p ~/.openhands-state && echo '{"language":"en","agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":false,"llm_model":"lm_studio/'$LMSTUDIO_MODEL_NAME'","llm_api_key":"dummy","llm_base_url":"'$LMSTUDIO_URL/v1'","remote_runtime_resource_factor":null,"github_token":null,"enable_default_condenser":true,"user_consents_to_analytics":true}' > ~/.openhands-state/settings.json
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.41
docker.all-hands.dev/all-hands-ai/openhands:0.43
```
Once your server is running -- you can visit `http://localhost:3000` in your browser to use OpenHands with local Devstral model:
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.41
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.43
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
+26 -5
View File
@@ -1,6 +1,6 @@
---
title: Getting Started
description: Getting started with running OpenHands locally.
description: Getting started with running OpenHands on your own.
---
## Recommended Methods for Running Openhands on Your Local System
@@ -62,17 +62,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
### Start the App
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.40
docker.all-hands.dev/all-hands-ai/openhands:0.43
```
You'll find OpenHands running at http://localhost:3000!
@@ -109,10 +109,32 @@ OpenHands requires an API key to access most language models. Here's how to get
</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>
<Accordion title="Local LLM (e.g. LM Studio, llama.cpp, Ollama)">
If your local LLM server isnt behind an authentication proxy, you can enter any value as the API key (e.g. `local-key`, `test123`) — it wont be used.
</Accordion>
</AccordionGroup>
Consider setting usage limits to control costs.
#### Using a Local LLM
<Note>
Effective use of local models for agent tasks requires capable hardware, along with models specifically tuned for instruction-following and agent-style behavior.
</Note>
To run OpenHands with a locally hosted language model instead of a cloud provider, see the [Local LLMs guide](/usage/llms/local-llms) for setup instructions.
#### Setting Up Search Engine
OpenHands can be configured to use a search engine to allow the agent to search the web for information when needed.
@@ -124,7 +146,6 @@ To enable search functionality in OpenHands:
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
@@ -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.
+34 -2
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`.
```
+18 -1
View File
@@ -71,10 +71,27 @@ 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.
### Enabling LLM-Based Editor Tools
The LLM-Based Editor tool (currently supported only for SWE-Bench) can be enabled by setting:
```bash
export ENABLE_LLM_EDITOR=true
```
You can set the config for the Editor LLM as:
```toml
[llm.draft_editor]
base_url = "http://localhost:9002/v1"
model = "hosted_vllm/lite_coder_qwen_editor_3B"
api_key = ""
temperature = 0.7
max_input_tokens = 10500
max_output_tokens = 10500
```
## Supported Benchmarks
The OpenHands evaluation harness supports a wide variety of benchmarks across [software engineering](#software-engineering), [web browsing](#web-browsing), [miscellaneous assistance](#misc-assistance), and [real-world](#real-world) tasks.
+7 -2
View File
@@ -42,7 +42,7 @@ from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
get_parser
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
@@ -62,6 +62,7 @@ from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
ENABLE_LLM_EDITOR = os.environ.get('ENABLE_LLM_EDITOR', 'false').lower() == 'true'
BenchMode = Literal['swe', 'swt', 'swt-ci']
@@ -254,15 +255,19 @@ def get_config(
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
)
)
# get 'draft_editor' config if exists
config.set_llm_config(get_llm_config_arg('draft_editor'), 'draft_editor')
agent_config = AgentConfig(
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
enable_llm_editor=ENABLE_LLM_EDITOR,
enable_mcp=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
@@ -4,7 +4,6 @@ import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import type { Message } from "#/message";
import { SUGGESTIONS } from "#/utils/suggestions";
import { WsClientProviderStatus } from "#/context/ws-client-provider";
import { ChatInterface } from "#/components/features/chat/chat-interface";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -19,7 +18,7 @@ describe("Empty state", () => {
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
useWsClient: vi.fn(() => ({
send: sendMock,
status: WsClientProviderStatus.CONNECTED,
status: "CONNECTED",
isLoadingMessages: false,
})),
}));
@@ -64,7 +63,7 @@ describe("Empty state", () => {
// this is to test that the message is in the UI before the socket is called
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: WsClientProviderStatus.CONNECTED,
status: "CONNECTED",
isLoadingMessages: false,
}));
const user = userEvent.setup();
@@ -87,7 +86,7 @@ describe("Empty state", () => {
async () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: WsClientProviderStatus.CONNECTED,
status: "CONNECTED",
isLoadingMessages: false,
}));
const user = userEvent.setup();
@@ -101,7 +100,7 @@ describe("Empty state", () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: WsClientProviderStatus.CONNECTED,
status: "CONNECTED",
isLoadingMessages: false,
}));
rerender(<ChatInterface />);
@@ -478,7 +478,7 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
status="RUNNING"
conversationStatus="RUNNING"
/>,
);
@@ -48,6 +48,7 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -60,6 +61,7 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -72,6 +74,7 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -158,6 +161,7 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -170,6 +174,7 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -182,6 +187,7 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -65,6 +65,7 @@ describe("WsClientProvider", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "RUNNING" as const,
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
}}},
@@ -334,10 +334,7 @@ describe("Settings 404", () => {
renderHomeScreen();
// small hack to wait for the modal to not appear
await expect(
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
).rejects.toThrow();
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
});
});
@@ -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",
},
],
},
]);
+18
View File
@@ -0,0 +1,18 @@
import { heroui } from "@heroui/react";
export default heroui({
defaultTheme: "dark",
layout: {
radius: {
small: "5px",
large: "20px",
},
},
themes: {
dark: {
colors: {
primary: "#4465DB",
},
},
},
});
+3642 -2960
View File
File diff suppressed because it is too large Load Diff
+25 -24
View File
@@ -1,37 +1,39 @@
{
"name": "openhands-frontend",
"version": "0.41.0",
"version": "0.43.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "2.7.8",
"@heroui/react": "^2.8.0-beta.7",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.6.1",
"@react-router/serve": "^7.6.1",
"@react-router/node": "^7.6.2",
"@react-router/serve": "^7.6.2",
"@react-types/shared": "^3.29.1",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.0",
"@tanstack/react-query": "^5.77.2",
"@vitejs/plugin-react": "^4.4.0",
"@stripe/stripe-js": "^7.3.1",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.80.7",
"@vitejs/plugin-react": "^4.5.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.9.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.14.0",
"framer-motion": "^12.17.3",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.28",
"jose": "^6.0.11",
"lucide-react": "^0.511.0",
"lucide-react": "^0.514.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.245.2",
"posthog-js": "^1.251.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -40,15 +42,15 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.6.1",
"react-router": "^7.6.2",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.0",
"tailwind-merge": "^3.3.1",
"vite": "^6.3.5",
"web-vitals": "^5.0.1",
"web-vitals": "^5.0.3",
"ws": "^8.18.2"
},
"scripts": {
@@ -82,23 +84,23 @@
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.52.0",
"@react-router/dev": "^7.6.1",
"@playwright/test": "^1.53.0",
"@react-router/dev": "^7.6.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.15.21",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@types/node": "^24.0.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/coverage-v8": "^3.2.3",
"autoprefixer": "^10.4.21",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
@@ -113,12 +115,11 @@
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7",
"jsdom": "^26.1.0",
"lint-staged": "^16.0.0",
"lint-staged": "^16.1.0",
"msw": "^2.6.6",
"postcss": "^8.5.2",
"prettier": "^3.5.3",
"stripe": "^18.1.1",
"tailwindcss": "^3.4.17",
"stripe": "^18.2.1",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
+2 -3
View File
@@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
},
}
};
@@ -117,6 +117,9 @@ const EXCLUDED_TECHNICAL_STRINGS = [
"edit-secret-form", // Test ID for secret form
"search-api-key-input", // Input name for search API key
"noopener,noreferrer", // Options for window.open
"STATUS$READY",
"STATUS$STOPPED",
"STATUS$ERROR",
];
function isExcludedTechnicalString(str) {
+56 -1
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);
},
);
+31
View File
@@ -11,6 +11,8 @@ import {
GetTrajectoryResponse,
GitChangeDiff,
GitChange,
GetMicroagentsResponse,
GetMicroagentPromptResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
@@ -393,6 +395,35 @@ class OpenHands {
return data;
}
/**
* Get the available microagents associated with a conversation
* @param conversationId The ID of the conversation
* @returns The available microagents associated with the conversation
*/
static async getMicroagents(
conversationId: string,
): Promise<GetMicroagentsResponse> {
const url = `${this.getConversationUrl(conversationId)}/microagents`;
const { data } = await openHands.get<GetMicroagentsResponse>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
static async getMicroagentPrompt(
conversationId: string,
eventId: number,
): Promise<string> {
const { data } = await openHands.get<GetMicroagentPromptResponse>(
`/api/conversations/${conversationId}/remember_prompt`,
{
params: { event_id: eventId },
},
);
return data.prompt;
}
}
export default OpenHands;
+25 -2
View File
@@ -1,4 +1,5 @@
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
import { ConversationStatus } from "#/types/conversation-status";
import { RuntimeStatus } from "#/types/runtime-status";
export interface ErrorResponse {
error: string;
@@ -80,7 +81,8 @@ export interface Conversation {
git_provider: string | null;
last_updated_at: string;
created_at: string;
status: ProjectStatus;
status: ConversationStatus;
runtime_status: RuntimeStatus | null;
trigger?: ConversationTrigger;
url: string | null;
session_api_key: string | null;
@@ -102,3 +104,24 @@ export interface GitChangeDiff {
modified: string;
original: string;
}
export interface InputMetadata {
name: string;
description: string;
}
export interface Microagent {
name: string;
type: "repo" | "knowledge";
content: string;
triggers: string[];
}
export interface GetMicroagentsResponse {
microagents: Microagent[];
}
export interface GetMicroagentPromptResponse {
status: string;
prompt: string;
}
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" style="enable-background:new 0 0 468 222.5" version="1.1" viewBox="0 0 468 222.5">
<style>
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#635bff}
</style>
<path d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z" class="st0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,68 +0,0 @@
import { I18nKey } from "#/i18n/declaration";
import { AgentState } from "#/types/agent-state";
export enum IndicatorColor {
BLUE = "bg-blue-500",
GREEN = "bg-green-500",
ORANGE = "bg-orange-500",
YELLOW = "bg-yellow-500",
RED = "bg-red-500",
DARK_ORANGE = "bg-orange-800",
}
export const AGENT_STATUS_MAP: {
[k: string]: { message: string; indicator: IndicatorColor };
} = {
[AgentState.INIT]: {
message: I18nKey.CHAT_INTERFACE$AGENT_INIT_MESSAGE,
indicator: IndicatorColor.BLUE,
},
[AgentState.RUNNING]: {
message: I18nKey.CHAT_INTERFACE$AGENT_RUNNING_MESSAGE,
indicator: IndicatorColor.GREEN,
},
[AgentState.AWAITING_USER_INPUT]: {
message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE,
indicator: IndicatorColor.BLUE,
},
[AgentState.PAUSED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE,
indicator: IndicatorColor.YELLOW,
},
[AgentState.LOADING]: {
message: I18nKey.CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE,
indicator: IndicatorColor.DARK_ORANGE,
},
[AgentState.STOPPED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_STOPPED_MESSAGE,
indicator: IndicatorColor.RED,
},
[AgentState.FINISHED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_FINISHED_MESSAGE,
indicator: IndicatorColor.GREEN,
},
[AgentState.REJECTED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_REJECTED_MESSAGE,
indicator: IndicatorColor.YELLOW,
},
[AgentState.ERROR]: {
message: I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE,
indicator: IndicatorColor.RED,
},
[AgentState.AWAITING_USER_CONFIRMATION]: {
message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE,
indicator: IndicatorColor.ORANGE,
},
[AgentState.USER_CONFIRMED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE,
indicator: IndicatorColor.GREEN,
},
[AgentState.USER_REJECTED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE,
indicator: IndicatorColor.RED,
},
[AgentState.RATE_LIMITED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE,
indicator: IndicatorColor.YELLOW,
},
};
@@ -132,7 +132,7 @@ export function ChatInput({
maxRows={maxRows}
data-dragging-over={isDraggingOver}
className={cn(
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none outline-none ring-0",
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none outline-hidden ring-0",
"transition-all duration-200 ease-in-out",
isDraggingOver
? "bg-neutral-600/50 rounded-lg px-2"
@@ -114,7 +114,7 @@ export function ExpandableMessage({
{t(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS)}
</div>
<Link
className="mt-2 mb-2 w-full h-10 rounded flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
className="mt-2 mb-2 w-full h-10 rounded-sm flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
to="/settings/billing"
>
{t(I18nKey.BILLING$CLICK_TO_TOP_UP)}
@@ -1,21 +1,14 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { showErrorToast } from "#/utils/error-handler";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import {
AGENT_STATUS_MAP,
IndicatorColor,
} from "../../agent-status-map.constant";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useWsClient } from "#/context/ws-client-provider";
import { useNotification } from "#/hooks/useNotification";
import { browserTab } from "#/utils/browser-tab";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { getIndicatorColor, getStatusCode } from "#/utils/status";
const notificationStates = [
AgentState.AWAITING_USER_INPUT,
@@ -27,39 +20,61 @@ export function AgentStatusBar() {
const { t, i18n } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curStatusMessage } = useSelector((state: RootState) => state.status);
const { status } = useWsClient();
const { notify } = useNotification();
const { webSocketStatus } = useWsClient();
const { data: conversation } = useActiveConversation();
const indicatorColor = getIndicatorColor(
webSocketStatus,
conversation?.status || null,
conversation?.runtime_status || null,
curAgentState,
);
const statusCode = getStatusCode(
curStatusMessage,
webSocketStatus,
conversation?.status || null,
conversation?.runtime_status || null,
curAgentState,
);
const { notify } = useNotification();
const [statusMessage, setStatusMessage] = React.useState<string>("");
const updateStatusMessage = () => {
// Show error toast if required
React.useEffect(() => {
if (curStatusMessage?.type !== "error") {
return;
}
let message = curStatusMessage.message || "";
if (curStatusMessage?.id) {
const id = curStatusMessage.id.trim();
if (id === "STATUS$READY") {
message = "awaiting_user_input";
}
if (i18n.exists(id)) {
message = t(curStatusMessage.id.trim()) || message;
}
}
if (curStatusMessage?.type === "error") {
showErrorToast({
message,
source: "agent-status",
metadata: { ...curStatusMessage },
});
return;
}
if (message.trim()) {
setStatusMessage(message);
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
}
};
React.useEffect(() => {
updateStatusMessage();
showErrorToast({
message,
source: "agent-status",
metadata: { ...curStatusMessage },
});
}, [curStatusMessage.id]);
// Handle notify
React.useEffect(() => {
if (notificationStates.includes(curAgentState)) {
const message = t(statusCode);
notify(message, {
body: t(`Agent state changed to ${curAgentState}`),
playSound: true,
});
// Update browser tab if window exists and is not focused
if (typeof document !== "undefined" && !document.hasFocus()) {
browserTab.startNotification(message);
}
}
}, [curAgentState, statusCode]);
// Handle window focus/blur
React.useEffect(() => {
if (typeof window === "undefined") return undefined;
@@ -75,42 +90,13 @@ export function AgentStatusBar() {
};
}, []);
const [indicatorColor, setIndicatorColor] = React.useState<string>(
AGENT_STATUS_MAP[curAgentState].indicator,
);
React.useEffect(() => {
if (conversation?.status === "STARTING") {
setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME));
setIndicatorColor(IndicatorColor.RED);
} else if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage(t(I18nKey.STATUS$WEBSOCKET_CLOSED));
setIndicatorColor(IndicatorColor.RED);
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
setIndicatorColor(AGENT_STATUS_MAP[curAgentState].indicator);
if (notificationStates.includes(curAgentState)) {
const message = t(AGENT_STATUS_MAP[curAgentState].message);
notify(t(AGENT_STATUS_MAP[curAgentState].message), {
body: t(`Agent state changed to ${curAgentState}`),
playSound: true,
});
// Update browser tab if window exists and is not focused
if (typeof document !== "undefined" && !document.hasFocus()) {
browserTab.startNotification(message);
}
}
}
}, [curAgentState, status, notify, t, conversation?.status]);
return (
<div className="flex flex-col items-center">
<div className="flex items-center bg-base-secondary px-2 py-1 text-gray-400 rounded-[100px] text-sm gap-[6px]">
<div
className={`w-2 h-2 rounded-full animate-pulse ${indicatorColor}`}
/>
<span className="text-sm text-stone-400">{t(statusMessage)}</span>
<span className="text-sm text-stone-400">{t(statusCode)}</span>
</div>
</div>
);
@@ -30,7 +30,7 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
title={conversation?.title ?? ""}
lastUpdatedAt={conversation?.created_at ?? ""}
selectedRepository={conversation?.selected_repository ?? null}
status={conversation?.status}
conversationStatus={conversation?.status}
conversationId={conversation?.conversation_id}
/>
</div>
@@ -1,7 +1,9 @@
import { useTranslation } from "react-i18next";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { cn } from "#/utils/utils";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
import { I18nKey } from "#/i18n/declaration";
interface ConversationCardContextMenuProps {
onClose: () => void;
@@ -9,6 +11,7 @@ interface ConversationCardContextMenuProps {
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onShowAgentTools?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onShowMicroagents?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
position?: "top" | "bottom";
}
@@ -19,9 +22,11 @@ export function ConversationCardContextMenu({
onEdit,
onDisplayCost,
onShowAgentTools,
onShowMicroagents,
onDownloadViaVSCode,
position = "bottom",
}: ConversationCardContextMenuProps) {
const { t } = useTranslation();
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
return (
@@ -68,6 +73,14 @@ export function ConversationCardContextMenu({
Show Agent Tools & Metadata
</ContextMenuListItem>
)}
{onShowMicroagents && (
<ContextMenuListItem
testId="show-microagents-button"
onClick={onShowMicroagents}
>
{t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
</ContextMenuListItem>
)}
</ContextMenu>
);
}
@@ -4,13 +4,11 @@ import posthog from "posthog-js";
import { useTranslation } from "react-i18next";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationRepoLink } from "./conversation-repo-link";
import {
ProjectStatus,
ConversationStateIndicator,
} from "./conversation-state-indicator";
import { ConversationStateIndicator } from "./conversation-state-indicator";
import { EllipsisButton } from "./ellipsis-button";
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
import { SystemMessageModal } from "./system-message-modal";
import { MicroagentsModal } from "./microagents-modal";
import { cn } from "#/utils/utils";
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
@@ -19,6 +17,7 @@ import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { useWsClient } from "#/context/ws-client-provider";
import { isSystemMessage } from "#/types/core/guards";
import { ConversationStatus } from "#/types/conversation-status";
interface ConversationCardProps {
onClick?: () => void;
@@ -30,7 +29,7 @@ interface ConversationCardProps {
selectedRepository: string | null;
lastUpdatedAt: string; // ISO 8601
createdAt?: string; // ISO 8601
status?: ProjectStatus;
conversationStatus?: ConversationStatus;
variant?: "compact" | "default";
conversationId?: string; // Optional conversation ID for VS Code URL
}
@@ -49,7 +48,7 @@ export function ConversationCard({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
lastUpdatedAt,
createdAt,
status = "STOPPED",
conversationStatus = "STOPPED",
variant = "default",
conversationId,
}: ConversationCardProps) {
@@ -59,6 +58,8 @@ export function ConversationCard({
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
const [microagentsModalVisible, setMicroagentsModalVisible] =
React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const systemMessage = parsedEvents.find(isSystemMessage);
@@ -142,6 +143,13 @@ export function ConversationCard({
setSystemModalVisible(true);
};
const handleShowMicroagents = (
event: React.MouseEvent<HTMLButtonElement>,
) => {
event.stopPropagation();
setMicroagentsModalVisible(true);
};
React.useEffect(() => {
if (titleMode === "edit") {
inputRef.current?.focus();
@@ -196,7 +204,9 @@ export function ConversationCard({
</div>
<div className="flex items-center">
<ConversationStateIndicator status={status} />
<ConversationStateIndicator
conversationStatus={conversationStatus}
/>
{hasContextMenu && (
<div className="pl-2">
<EllipsisButton
@@ -225,6 +235,11 @@ export function ConversationCard({
? handleShowAgentTools
: undefined
}
onShowMicroagents={
showOptions && conversationId
? handleShowMicroagents
: undefined
}
position={variant === "compact" ? "top" : "bottom"}
/>
)}
@@ -367,6 +382,13 @@ export function ConversationCard({
onClose={() => setSystemModalVisible(false)}
systemMessage={systemMessage ? systemMessage.args : null}
/>
{microagentsModalVisible && (
<MicroagentsModal
onClose={() => setMicroagentsModalVisible(false)}
conversationId={conversationId}
/>
)}
</>
);
}
@@ -91,7 +91,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
selectedRepository={project.selected_repository}
lastUpdatedAt={project.last_updated_at}
createdAt={project.created_at}
status={project.status}
conversationStatus={project.status}
conversationId={project.conversation_id}
/>
)}
@@ -1,26 +1,27 @@
import ColdIcon from "./state-indicators/cold.svg?react";
import { ConversationStatus } from "#/types/conversation-status";
import RunningIcon from "./state-indicators/running.svg?react";
import StartingIcon from "./state-indicators/starting.svg?react";
import StoppedIcon from "./state-indicators/stopped.svg?react";
type SVGIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
export type ProjectStatus = "RUNNING" | "STOPPED" | "STARTING";
const INDICATORS: Record<ProjectStatus, SVGIcon> = {
STOPPED: ColdIcon,
const CONVERSATION_STATUS_INDICATORS: Record<ConversationStatus, SVGIcon> = {
STOPPED: StoppedIcon,
RUNNING: RunningIcon,
STARTING: ColdIcon,
STARTING: StartingIcon,
};
interface ConversationStateIndicatorProps {
status: ProjectStatus;
conversationStatus: ConversationStatus;
}
export function ConversationStateIndicator({
status,
conversationStatus,
}: ConversationStateIndicatorProps) {
const StateIcon = INDICATORS[status];
const StateIcon = CONVERSATION_STATUS_INDICATORS[conversationStatus];
return (
<div data-testid={`${status}-indicator`}>
<div data-testid={`${conversationStatus}-indicator`}>
<StateIcon />
</div>
);
@@ -0,0 +1,142 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, ChevronRight } from "lucide-react";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { I18nKey } from "#/i18n/declaration";
import { useConversationMicroagents } from "#/hooks/query/use-conversation-microagents";
interface MicroagentsModalProps {
onClose: () => void;
conversationId: string | undefined;
}
export function MicroagentsModal({
onClose,
conversationId,
}: MicroagentsModalProps) {
const { t } = useTranslation();
const [expandedAgents, setExpandedAgents] = useState<Record<string, boolean>>(
{},
);
const {
data: microagents,
isLoading,
isError,
} = useConversationMicroagents({
conversationId,
enabled: true,
});
const toggleAgent = (agentName: string) => {
setExpandedAgents((prev) => ({
...prev,
[agentName]: !prev[agentName],
}));
};
return (
<ModalBackdrop onClose={onClose}>
<ModalBody
width="medium"
className="max-h-[80vh] flex flex-col items-start"
testID="microagents-modal"
>
<div className="flex flex-col gap-6 w-full">
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
</div>
<div className="w-full h-[60vh] overflow-auto rounded-md">
{isLoading && (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
</div>
)}
{!isLoading &&
(isError || !microagents || microagents.length === 0) && (
<div className="flex items-center justify-center h-full p-4">
<p className="text-gray-400">
{isError
? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
: t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
</p>
</div>
)}
{!isLoading && microagents && microagents.length > 0 && (
<div className="p-2 space-y-3">
{microagents.map((agent) => {
const isExpanded = expandedAgents[agent.name] || false;
return (
<div key={agent.name} className="rounded-md overflow-hidden">
<button
type="button"
onClick={() => toggleAgent(agent.name)}
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<h3 className="font-bold text-gray-100">
{agent.name}
</h3>
</div>
<div className="flex items-center">
<span className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
{agent.type === "repo" ? "Repository" : "Knowledge"}
</span>
<span className="text-gray-300">
{isExpanded ? (
<ChevronDown size={18} />
) : (
<ChevronRight size={18} />
)}
</span>
</div>
</button>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
{agent.triggers && agent.triggers.length > 0 && (
<div className="mt-2 mb-3">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
</h4>
<div className="flex flex-wrap gap-1">
{agent.triggers.map((trigger) => (
<span
key={trigger}
className="px-2 py-1 text-xs rounded-full bg-blue-900"
>
{trigger}
</span>
))}
</div>
</div>
)}
<div className="mt-2">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
</h4>
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
{agent.content ||
t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
</pre>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
</ModalBody>
</ModalBackdrop>
);
}
@@ -1,4 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 16.8599C13.4183 16.8599 17 13.2781 17 8.85986C17 4.44159 13.4183 0.859863 9 0.859863C4.58172 0.859863 1 4.44159 1 8.85986C1 13.2781 4.58172 16.8599 9 16.8599Z" fill="#779FD4"/>
<path d="M4.61035 8.43014L7.86035 12.0301L13.3904 6.64014" stroke="#231F20" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

@@ -1,4 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.76039 6.99002C8.478 6.99002 9.87039 5.59763 9.87039 3.88002C9.87039 2.16241 8.478 0.77002 6.76039 0.77002C5.04279 0.77002 3.65039 2.16241 3.65039 3.88002C3.65039 5.59763 5.04279 6.99002 6.76039 6.99002Z" fill="#FFE165"/>
<path d="M1.0802 17.0799C1.0802 17.0799 0.610196 11.5499 3.0102 9.67992C4.7902 8.29992 7.3302 9.44992 9.7802 7.95992C11.5802 6.86992 13.6102 4.10992 14.5202 2.49992C14.9302 1.77992 15.9102 1.62992 16.6102 2.05992C17.3802 2.51992 17.6102 3.53992 17.1102 4.28992C16.2302 5.58992 14.1802 8.85992 13.1202 10.3699C10.7602 13.7599 11.4302 17.0799 11.4302 17.0799H1.0702H1.0802Z" fill="#FFE165"/>
</svg>

Before

Width:  |  Height:  |  Size: 726 B

@@ -1,4 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.87012 2.08984C9.87012 1.53756 9.4224 1.08984 8.87012 1.08984C8.31783 1.08984 7.87012 1.53756 7.87012 2.08984V8.08984C7.87012 8.64213 8.31783 9.08984 8.87012 9.08984C9.4224 9.08984 9.87012 8.64213 9.87012 8.08984V2.08984Z" fill="#60BB46"/>
<path d="M10.8702 2.50988V2.64988C10.8702 3.01988 11.0702 3.36988 11.4102 3.51988C13.6802 4.51988 15.2202 6.88988 14.9702 9.56988C14.7002 12.5599 12.1002 14.9599 9.09021 15.0099C5.74021 15.0599 2.99021 12.3499 2.99021 9.00988C2.99021 6.65988 4.35021 4.62988 6.31021 3.64988C6.64021 3.48988 6.86021 3.16988 6.86021 2.80988V2.63988C6.86021 1.94988 6.14021 1.51988 5.51021 1.81988C2.42021 3.30988 0.430214 6.71988 1.12021 10.5199C1.69021 13.6799 4.22021 16.2499 7.37021 16.8699C12.4902 17.8699 16.9802 13.9699 16.9802 9.02988C16.9802 5.71988 14.9702 2.88988 12.1002 1.66988C11.5102 1.41988 10.8502 1.88988 10.8502 2.52988L10.8702 2.50988Z" fill="#60BB46"/>
</svg>

Before

Width:  |  Height:  |  Size: 1008 B

@@ -101,7 +101,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
name="email"
type="email"
placeholder={t(I18nKey.FEEDBACK$EMAIL_PLACEHOLDER)}
className="bg-[#27272A] px-3 py-[10px] rounded"
className="bg-[#27272A] px-3 py-[10px] rounded-sm"
/>
</label>
@@ -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;
}
@@ -6,7 +6,7 @@ export function BranchErrorState() {
return (
<div
data-testid="branch-dropdown-error"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm text-red-500"
>
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_BRANCHES")}</span>
</div>
@@ -7,7 +7,7 @@ export function BranchLoadingState() {
return (
<div
data-testid="branch-dropdown-loading"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
>
<Spinner size="sm" />
<span className="text-sm">{t("HOME$LOADING_BRANCHES")}</span>
@@ -6,7 +6,7 @@ export function RepositoryErrorState() {
return (
<div
data-testid="repo-dropdown-error"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm text-red-500"
>
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_REPOSITORIES")}</span>
</div>
@@ -7,7 +7,7 @@ export function RepositoryLoadingState() {
return (
<div
data-testid="repo-dropdown-loading"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
>
<Spinner size="sm" />
<span className="text-sm">{t("HOME$LOADING_REPOSITORIES")}</span>
@@ -12,7 +12,7 @@ export function Thumbnail({ src, size = "small" }: ThumbnailProps) {
alt=""
src={src}
className={cn(
"rounded object-cover",
"rounded-sm object-cover",
size === "small" && "w-[62px] h-[62px]",
size === "large" && "w-[100px] h-[100px]",
)}
@@ -9,6 +9,7 @@ import { BrandButton } from "../settings/brand-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { amountIsValid } from "#/utils/amount-is-valid";
import { I18nKey } from "#/i18n/declaration";
import { PoweredByStripeTag } from "./powered-by-stripe-tag";
export function PaymentForm() {
const { t } = useTranslation();
@@ -42,7 +43,7 @@ export function PaymentForm() {
>
<div
className={cn(
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded px-3 py-2",
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded-sm px-3 py-2",
"text-[28px] leading-8 -tracking-[0.02em] font-bold",
)}
>
@@ -79,6 +80,7 @@ export function PaymentForm() {
{t(I18nKey.PAYMENT$ADD_CREDIT)}
</BrandButton>
{isPending && <LoadingSpinner size="small" />}
<PoweredByStripeTag />
</div>
</div>
</form>
@@ -0,0 +1,16 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import stripeLogo from "#/assets/stripe.svg";
export function PoweredByStripeTag() {
const { t } = useTranslation();
return (
<div className="flex flex-row items-center">
<span className="text-medium font-semi-bold">
{t(I18nKey.BILLING$POWERED_BY)}
</span>
<img src={stripeLogo} alt="Stripe" className="h-8" />
</div>
);
}
@@ -32,7 +32,7 @@ export function BrandButton({
type={type}
onClick={onClick}
className={cn(
"w-fit p-2 text-sm rounded disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
"w-fit p-2 text-sm rounded-sm disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
variant === "primary" && "bg-primary text-[#0D0F11]",
variant === "secondary" && "border border-primary text-primary",
variant === "danger" && "bg-red-600 text-white hover:bg-red-700",
@@ -69,7 +69,7 @@ export function MCPJsonEditor({ mcpConfig, onChange }: MCPJsonEditorProps) {
{t(I18nKey.SETTINGS$MCP_CONFIG_DESCRIPTION)}
</div>
<textarea
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-none"
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-hidden"
value={configText}
onChange={handleTextChange}
spellCheck="false"
@@ -158,7 +158,7 @@ export function SecretForm({
required
className={cn(
"resize-none",
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"bg-tertiary border border-[#717888] rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
rows={8}
@@ -177,7 +177,7 @@ export function SecretForm({
defaultValue={secretDescription}
className={cn(
"resize-none",
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"bg-tertiary border border-[#717888] rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
/>
@@ -63,7 +63,7 @@ export function SettingsDropdownInput({
inputProps={{
classNames: {
inputWrapper:
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
},
}}
defaultFilter={defaultFilter}
@@ -62,7 +62,7 @@ export function SettingsInput({
required={required}
pattern={pattern}
className={cn(
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
/>
@@ -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
@@ -16,7 +16,7 @@ export function ReplaySuggestionBox({ onChange }: ReplaySuggestionBoxProps) {
htmlFor="import-trajectory"
className="w-full flex justify-center"
>
<span className="border-2 border-dashed border-neutral-600 rounded px-2 py-1 cursor-pointer">
<span className="border-2 border-dashed border-neutral-600 rounded-sm px-2 py-1 cursor-pointer">
{t(I18nKey.LANDING$UPLOAD_TRAJECTORY)}
</span>
<input
@@ -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>
);
@@ -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>
);
}
@@ -18,7 +18,7 @@ export function EditorActionButton({
onClick={onClick}
disabled={disabled}
className={cn(
"text-sm py-0.5 rounded w-20",
"text-sm py-0.5 rounded-sm w-20",
"hover:bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
@@ -31,7 +31,7 @@ export function ModalButton({
disabled={disabled}
onClick={onClick}
className={clsx(
variant === "default" && "text-sm font-[500] py-[10px] rounded",
variant === "default" && "text-sm font-[500] py-[10px] rounded-sm",
variant === "text-like" && "text-xs leading-4 font-normal",
icon && "flex items-center justify-center gap-2",
disabled && "opacity-50 cursor-not-allowed",
@@ -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>
@@ -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>
@@ -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;
}
@@ -36,7 +36,7 @@ export function CustomInput({
required={required}
defaultValue={defaultValue}
type={type}
className="bg-[#27272A] text-xs py-[10px] px-3 rounded"
className="bg-[#27272A] text-xs py-[10px] px-3 rounded-sm"
/>
</label>
);
@@ -16,7 +16,7 @@ export function ErrorToast({ id, error }: ErrorToastProps) {
<button
type="button"
onClick={() => toast.dismiss(id)}
className="bg-neutral-500 px-1 rounded h-full"
className="bg-neutral-500 px-1 rounded-sm h-full"
>
{t(I18nKey.ERROR_TOAST$CLOSE_BUTTON_LABEL)}
</button>
@@ -269,19 +269,19 @@ function SecurityInvariant() {
<hr className="border-t border-neutral-600 my-2" />
<ul className="space-y-2">
<div
className={`cursor-pointer p-2 rounded ${activeSection === "logs" && "bg-neutral-600"}`}
className={`cursor-pointer p-2 rounded-sm ${activeSection === "logs" && "bg-neutral-600"}`}
onClick={() => setActiveSection("logs")}
>
{t(I18nKey.INVARIANT$LOG_LABEL)}
</div>
<div
className={`cursor-pointer p-2 rounded ${activeSection === "policy" && "bg-neutral-600"}`}
className={`cursor-pointer p-2 rounded-sm ${activeSection === "policy" && "bg-neutral-600"}`}
onClick={() => setActiveSection("policy")}
>
{t(I18nKey.INVARIANT$POLICY_LABEL)}
</div>
<div
className={`cursor-pointer p-2 rounded ${activeSection === "settings" && "bg-neutral-600"}`}
className={`cursor-pointer p-2 rounded-sm ${activeSection === "settings" && "bg-neutral-600"}`}
onClick={() => setActiveSection("settings")}
>
{t(I18nKey.INVARIANT$SETTINGS_LABEL)}
@@ -92,7 +92,7 @@ export function ModelSelector({
inputProps={{
classNames: {
inputWrapper:
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
},
}}
>
@@ -142,7 +142,7 @@ export function ModelSelector({
inputProps={{
classNames: {
inputWrapper:
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
},
}}
>
@@ -96,7 +96,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
testId="llm-api-key-help-anchor"
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
href="https://docs.all-hands.dev/usage/installation#getting-an-api-key"
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
/>
</div>
+27 -22
View File
@@ -29,6 +29,8 @@ import {
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
export type WebSocketStatus = "CONNECTING" | "CONNECTED" | "DISCONNECTED";
const hasValidMessageProperty = (obj: unknown): obj is { message: string } =>
typeof obj === "object" &&
obj !== null &&
@@ -67,14 +69,8 @@ const isMessageAction = (
): event is UserMessageAction | AssistantMessageAction =>
isUserMessage(event) || isAssistantMessage(event);
export enum WsClientProviderStatus {
CONNECTED,
DISCONNECTED,
CONNECTING,
}
interface UseWsClient {
status: WsClientProviderStatus;
webSocketStatus: WebSocketStatus;
isLoadingMessages: boolean;
events: Record<string, unknown>[];
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
@@ -82,7 +78,7 @@ interface UseWsClient {
}
const WsClientContext = React.createContext<UseWsClient>({
status: WsClientProviderStatus.DISCONNECTED,
webSocketStatus: "DISCONNECTED",
isLoadingMessages: true,
events: [],
parsedEvents: [],
@@ -139,9 +135,8 @@ export function WsClientProvider({
const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
const queryClient = useQueryClient();
const sioRef = React.useRef<Socket | null>(null);
const [status, setStatus] = React.useState(
WsClientProviderStatus.DISCONNECTED,
);
const [webSocketStatus, setWebSocketStatus] =
React.useState<WebSocketStatus>("DISCONNECTED");
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const [parsedEvents, setParsedEvents] = React.useState<
(OpenHandsAction | OpenHandsObservation)[]
@@ -162,7 +157,7 @@ export function WsClientProvider({
}
function handleConnect() {
setStatus(WsClientProviderStatus.CONNECTED);
setWebSocketStatus("CONNECTED");
removeErrorMessage();
}
@@ -261,7 +256,7 @@ export function WsClientProvider({
}
function handleDisconnect(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
setWebSocketStatus("DISCONNECTED");
const sio = sioRef.current;
if (!sio) {
return;
@@ -275,7 +270,7 @@ export function WsClientProvider({
function handleError(data: unknown) {
// set status
setStatus(WsClientProviderStatus.DISCONNECTED);
setWebSocketStatus("DISCONNECTED");
updateStatusWhenErrorMessagePresent(data);
setErrorMessage(
@@ -294,17 +289,14 @@ export function WsClientProvider({
// reset events when conversationId changes
setEvents([]);
setParsedEvents([]);
setStatus(WsClientProviderStatus.DISCONNECTED);
setWebSocketStatus("CONNECTING");
}, [conversationId]);
React.useEffect(() => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}
if (
!conversation ||
["STOPPED", "STARTING"].includes(conversation.status)
) {
if (conversation?.status !== "RUNNING" && !conversation?.runtime_status) {
return () => undefined; // conversation not yet loaded
}
@@ -314,6 +306,9 @@ export function WsClientProvider({
sio.disconnect();
}
// Set initial status...
setWebSocketStatus("CONNECTING");
const lastEvent = lastEventRef.current;
const query = {
latest_event_id: lastEvent?.id ?? -1,
@@ -348,7 +343,12 @@ export function WsClientProvider({
sio.off("connect_failed", handleError);
sio.off("disconnect", handleDisconnect);
};
}, [conversationId, conversation?.url, conversation?.status]);
}, [
conversationId,
conversation?.url,
conversation?.status,
conversation?.runtime_status,
]);
React.useEffect(
() => () => {
@@ -363,13 +363,18 @@ export function WsClientProvider({
const value = React.useMemo<UseWsClient>(
() => ({
status,
webSocketStatus,
isLoadingMessages: messageRateHandler.isUnderThreshold,
events,
parsedEvents,
send,
}),
[status, messageRateHandler.isUnderThreshold, events, parsedEvents],
[
webSocketStatus,
messageRateHandler.isUnderThreshold,
events,
parsedEvents,
],
);
return <WsClientContext value={value}>{children}</WsClientContext>;
@@ -17,6 +17,10 @@ export const useActiveConversation = () => {
useEffect(() => {
const conversation = userConversation.data;
OpenHands.setCurrentConversation(conversation || null);
}, [conversationId, userConversation.isFetched]);
}, [
conversationId,
userConversation.isFetched,
userConversation?.data?.status,
]);
return userConversation;
};

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