Compare commits

..

8 Commits

152 changed files with 4232 additions and 7472 deletions
+4 -22
View File
@@ -1,23 +1,5 @@
# NodeJS
frontend/node_modules
# 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
config.toml
.envrc
.env
.git
-6
View File
@@ -72,9 +72,3 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directories:
- "containers/*"
schedule:
interval: "weekly"
+1 -7
View File
@@ -44,13 +44,7 @@ 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.42-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.41-nikolaik`
## Develop inside Docker container
+3 -14
View File
@@ -18,17 +18,6 @@
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
<!-- Keep these links. Translations will automatically update with the README. -->
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=de">Deutsch</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=es">Español</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=fr">français</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ja">日本語</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ko">한국어</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=pt">Português</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ru">Русский</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=zh">中文</a>
<hr>
</div>
@@ -62,17 +51,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.42
docker.all-hands.dev/all-hands-ai/openhands:0.41
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+3 -3
View File
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.42
docker.all-hands.dev/all-hands-ai/openhands:0.41
```
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands
+15 -11
View File
@@ -1,16 +1,16 @@
ARG OPENHANDS_BUILD_VERSION=dev
FROM node:22.16.0-bookworm-slim AS frontend-builder
FROM node:21.7.2-bookworm-slim AS frontend-builder
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json ./
COPY ./frontend/package.json frontend/package-lock.json ./
RUN npm install -g npm@10.5.1
RUN npm ci
COPY frontend ./
COPY ./frontend ./
RUN npm run build
FROM python:3.12.10-slim AS base
FROM base AS backend-builder
FROM python:3.12.3-slim AS backend-builder
WORKDIR /app
ENV PYTHONPATH='/app'
@@ -22,18 +22,17 @@ ENV POETRY_NO_INTERACTION=1 \
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential \
&& python3 -m pip install poetry --break-system-packages
&& python3 -m pip install poetry==1.8.2 --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 base AS openhands-app
FROM python:3.12.3-slim AS openhands-app
WORKDIR /app
# re-declare for this section
ARG OPENHANDS_BUILD_VERSION
ARG OPENHANDS_BUILD_VERSION #re-declare for this section
ENV RUN_AS_OPENHANDS=true
# A random number--we need this to be different from the user's UID on the host machine
@@ -75,7 +74,12 @@ 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 pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
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
# 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
+1 -2
View File
@@ -11,12 +11,11 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.42-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.41-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
- "3000:3000"
network_mode: host
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
+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.42-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
-17
View File
@@ -1,17 +0,0 @@
# Setup
```
npm install -g mint
```
or
```
yarn global add mint
```
# Preview
```
mint dev
```
+49 -55
View File
@@ -34,8 +34,7 @@
"group": "Integrations",
"pages": [
"usage/cloud/github-installation",
"usage/cloud/gitlab-installation",
"usage/cloud/slack-installation"
"usage/cloud/gitlab-installation"
]
},
"usage/cloud/cloud-ui",
@@ -49,62 +48,13 @@
"usage/how-to/gui-mode",
"usage/how-to/cli-mode",
"usage/how-to/headless-mode",
"usage/how-to/github-action",
{
"group": "Advanced Configuration",
"pages": [
{
"group": "LLM Configuration",
"pages": [
"usage/llms/llms",
{
"group": "Providers",
"pages": [
"usage/llms/azure-llms",
"usage/llms/google-llms",
"usage/llms/groq",
"usage/llms/local-llms",
"usage/llms/litellm-proxy",
"usage/llms/openai-llms",
"usage/llms/openrouter"
]
}
]
},
{
"group": "Runtime Configuration",
"pages": [
"usage/runtimes/overview",
{
"group": "Providers",
"pages": [
"usage/runtimes/docker",
"usage/runtimes/remote",
"usage/runtimes/local",
{
"group": "Third-Party Providers",
"pages": [
"usage/runtimes/modal",
"usage/runtimes/daytona",
"usage/runtimes/runloop",
"usage/runtimes/e2b"
]
}
]
}
]
},
"usage/configuration-options",
"usage/how-to/custom-sandbox-guide",
"usage/search-engine-setup",
"usage/mcp"
]
}
"usage/how-to/github-action"
]
},
{
"group": "Customization",
"pages": [
"usage/prompting/prompting-best-practices",
"usage/prompting/repository",
{
"group": "Microagents",
@@ -119,9 +69,53 @@
]
},
{
"group": "Tips and Tricks",
"group": "Advanced Configuration",
"pages": [
"usage/prompting/prompting-best-practices"
{
"group": "LLM Configuration",
"pages": [
"usage/llms/llms",
{
"group": "Providers",
"pages": [
"usage/llms/azure-llms",
"usage/llms/google-llms",
"usage/llms/groq",
"usage/llms/local-llms",
"usage/llms/litellm-proxy",
"usage/llms/openai-llms",
"usage/llms/openrouter"
]
}
]
},
{
"group": "Runtime Configuration",
"pages": [
"usage/runtimes/overview",
{
"group": "Providers",
"pages": [
"usage/runtimes/docker",
"usage/runtimes/remote",
"usage/runtimes/local",
{
"group": "Third-Party Providers",
"pages": [
"usage/runtimes/modal",
"usage/runtimes/daytona",
"usage/runtimes/runloop",
"usage/runtimes/e2b"
]
}
]
}
]
},
"usage/configuration-options",
"usage/how-to/custom-sandbox-guide",
"usage/search-engine-setup",
"usage/mcp"
]
},
{
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 KiB

+79 -73
View File
@@ -1,11 +1,9 @@
---
title: Cloud API
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.
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.
---
For the available API endpoints, refer to the
[OpenHands API Reference](https://docs.all-hands.dev/api-reference).
For more detailed information about the API, refer to the [OpenHands API Reference](https://docs.all-hands.dev/swagger-ui/).
## Obtaining an API Key
@@ -18,7 +16,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/api-key-generation.png)
![API Key Generation](/static/img/docs/api-key-generation.png)
## API Usage
@@ -35,81 +33,87 @@ To start a new conversation with OpenHands to perform a task, you'll need to mak
#### Examples
<details>
<summary>cURL</summary>
<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>
```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="Python (with requests)">
```python
import requests
<details>
<summary>Python (with requests)</summary>
api_key = "YOUR_API_KEY"
url = "https://app.all-hands.dev/api/conversations"
```python
import requests
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
api_key = "YOUR_API_KEY"
url = "https://app.all-hands.dev/api/conversations"
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']}")
```
</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}`,
headers = {
"Authorization": f"Bearer {api_key}",
"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"
};
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)
});
response = requests.post(url, headers=headers, json=data)
conversation = response.json()
const conversation = await response.json();
print(f"Conversation Link: https://app.all-hands.dev/conversations/{conversation['conversation_id']}")
print(f"Status: {conversation['status']}")
```
</details>
console.log(`Conversation Link: https://app.all-hands.dev/conversations/${conversation.id}`);
console.log(`Status: ${conversation.status}`);
<details>
<summary>TypeScript/JavaScript (with fetch)</summary>
return conversation;
} catch (error) {
console.error("Error starting conversation:", error);
}
```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>
startConversation();
```
</details>
#### Response
@@ -141,12 +145,14 @@ GET https://app.all-hands.dev/api/conversations/{conversation_id}
#### Example
<Accordion title="cURL">
```bash
curl -X GET "https://app.all-hands.dev/api/conversations/{conversation_id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
</Accordion>
<details>
<summary>cURL</summary>
```bash
curl -X GET "https://app.all-hands.dev/api/conversations/{conversation_id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
</details>
#### Response
+1 -1
View File
@@ -26,7 +26,7 @@ The Settings page allows you to:
## Key Features
For an overview of the key features available inside a conversation, please refer to the [Key Features](/usage/key-features)
For an overview of the key features available inside a conversation, please refer to the [Key Features](../key-features)
section of the documentation.
## Next Steps
-6
View File
@@ -19,12 +19,6 @@ appropriate repository and branch you'd like OpenHands to work on. Then click on
![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
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
-52
View File
@@ -1,52 +0,0 @@
---
title: Slack Integration - Coming soon...
description: This guide walks you through installing the OpenHands Slack app.
---
<Warning>This integration is not live yet, but will be available soon.</Warning>
## Prerequisites
- You are a slack workspace admin
- Access to OpenHands Cloud
## Installation Steps
1. Log in to [OpenHands Cloud](https://app.all-hands.dev)
2. Click the button below to OpenHands Slack App <a target="_blank" href="https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,chat:write,users:read,channels:history,groups:history,mpim:history,im:history&user_scope=channels:history,groups:history,im:history,mpim:history"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
3. In the top right corner, select the workspace to install the OpenHands Slack app.
4. Review permissions and click allow
## Working With the Slack App
To start a new conversation, you can mention `@openhands` in a new message or a thread inside any Slack channel.
Once a conversation is started, all thread messages underneath it will be follow-up messages to OpenHands.
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message. You must be the user who started the conversation.
## Example conversation
### Start a new conversation, and select repo
Conversation is started by mentioning `@openhands`.
![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)
+5 -7
View File
@@ -1,11 +1,9 @@
---
title: CLI
description: The Command-Line Interface (CLI) provides a powerful interface that lets you engage with OpenHands
directly from your terminal.
title: CLI Mode
description: CLI mode provides a powerful interactive Command-Line Interface (CLI) that lets you engage with OpenHands directly from your terminal.
---
This mode is different from the [headless mode](/usage/how-to/headless-mode), which is non-interactive and better
for scripting.
This mode is different from the [headless mode](./headless-mode), which is non-interactive and better for scripting.
## Getting Started
@@ -46,7 +44,7 @@ poetry run python -m openhands.cli.main
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -55,7 +53,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.42 \
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
python -m openhands.cli.main --override-cli-mode true
```
+45 -64
View File
@@ -1,13 +1,14 @@
---
title: GUI
description: High level overview of the Graphical User Interface (GUI) in OpenHands.
title: GUI Mode
description: OpenHands provides a Graphical User Interface (GUI) mode for interacting with the AI assistant.
---
## Prerequisites
## Installation and Setup
- [OpenHands is running](/usage/local-setup)
1. Follow the installation instructions to install OpenHands.
2. After running the command, access OpenHands at [http://localhost:3000](http://localhost:3000).
## Overview
## Interacting with the GUI
### Initial Setup
@@ -18,23 +19,16 @@ description: High level overview of the Graphical User Interface (GUI) in OpenHa
3. Enter the corresponding `API Key` for your chosen provider.
4. Click `Save Changes` to apply the settings.
### Settings
### Version Control Tokens
You can use the Settings page at any time to:
OpenHands supports multiple version control providers. You can configure tokens for multiple providers simultaneously.
- 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
#### GitHub Token Setup
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if provided:
<AccordionGroup>
<Accordion title="Setting Up a GitHub Token">
<details>
<summary>Setting Up a GitHub Token</summary>
1. **Generate a Personal Access Token (PAT)**:
- On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
@@ -43,11 +37,16 @@ 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**:
- In the Settings page, navigate to the `Git` tab.
- Click the Settings button (gear icon).
- 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:
@@ -60,12 +59,15 @@ 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.
</Accordion>
</details>
<details>
<summary>Troubleshooting</summary>
<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.
@@ -79,15 +81,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.
</Accordion>
</AccordionGroup>
</details>
#### GitLab Setup
#### GitLab Token Setup
OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if provided:
<AccordionGroup>
<Accordion title="Setting Up a GitLab Token">
<details>
<summary>Setting Up a GitLab Token</summary>
1. **Generate a Personal Access Token (PAT)**:
- On GitLab, go to User Settings > Access Tokens.
- Create a new token with the following scopes:
@@ -97,17 +99,15 @@ 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**:
- In the Settings page, navigate to the `Git` tab.
- Click the Settings button (gear icon).
- Navigate to the `Git` tab.
- Paste your token in the `GitLab Token` field.
- Click `Save Changes` to apply the changes.
</details>
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>
<details>
<summary>Troubleshooting</summary>
<Accordion title="Troubleshooting">
Common issues and solutions:
- **Token Not Recognized**:
@@ -119,44 +119,25 @@ 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.
</Accordion>
</AccordionGroup>
</details>
#### Advanced Settings
### Advanced Settings
The `Advanced` settings allows configuration of additional LLM settings. Inside the Settings page, under the `LLM` tab,
toggle `Advanced` options to access additional 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.
- 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.
### Interacting with the AI
### 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!
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.
## 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).
## 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)
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.
+5 -6
View File
@@ -1,10 +1,9 @@
---
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.
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.
---
This is different from [the CLI](./cli-mode), which is interactive, and better for active development.
This is different from [CLI Mode](./cli-mode), which is interactive, and better for active development.
## With Python
@@ -32,7 +31,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +41,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.42 \
docker.all-hands.dev/all-hands-ai/openhands:0.41 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+1 -1
View File
@@ -12,7 +12,7 @@ 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 on Your Own
## Running OpenHands Locally
Run OpenHands on your local system and bring your own LLM and API key.
+5 -10
View File
@@ -14,28 +14,23 @@ recommendations for model selection. Our latest benchmarking results can be foun
Based on these findings and community feedback, these are the latest models that have been verified to work reasonably well with OpenHands:
### Cloud / API-Based Models
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) -- available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
to help others using the same provider!
For a full list of the providers and models available, please consult the
[litellm documentation](https://docs.litellm.ai/docs/providers).
<Warning>
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
limits and monitor usage.
</Warning>
### Local / Self-Hosted Models
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
to help others using the same provider!
- [mistralai/devstral-small](https://www.all-hands.dev/blog/devstral-a-new-state-of-the-art-open-model-for-coding-agents) (20 May 2025) -- also available through [OpenRouter](https://openrouter.ai/mistralai/devstral-small:free)
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) (31 March 2025) -- also available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
For a full list of the providers and models available, please consult the
[litellm documentation](https://docs.litellm.ai/docs/providers).
<Note>
Most current local and open source models are not as powerful. When using such models, you may see long
+4 -4
View File
@@ -54,25 +54,25 @@ Check [the installation guide](/usage/local-setup) to make sure you have all the
export LMSTUDIO_MODEL_NAME="imported-models/uncategorized/devstralq4_k_m.gguf" # <- Replace this with the model name you copied from LMStudio
export LMSTUDIO_URL="http://host.docker.internal:1234" # <- Replace this with the port from LMStudio
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
mkdir -p ~/.openhands-state && echo '{"language":"en","agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":false,"llm_model":"lm_studio/'$LMSTUDIO_MODEL_NAME'","llm_api_key":"dummy","llm_base_url":"'$LMSTUDIO_URL/v1'","remote_runtime_resource_factor":null,"github_token":null,"enable_default_condenser":true,"user_consents_to_analytics":true}' > ~/.openhands-state/settings.json
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.42
docker.all-hands.dev/all-hands-ai/openhands:0.41
```
Once your server is running -- you can visit `http://localhost:3000` in your browser to use OpenHands with local Devstral model:
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.42
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.41
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
+5 -18
View File
@@ -1,6 +1,6 @@
---
title: Getting Started
description: Getting started with running OpenHands on your own.
description: Getting started with running OpenHands locally.
---
## 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.42-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.42
docker.all-hands.dev/all-hands-ai/openhands:0.40
```
You'll find OpenHands running at http://localhost:3000!
@@ -117,24 +117,10 @@ OpenHands requires an API key to access most language models. Here's how to get
</Accordion>
<Accordion title="Local LLM (e.g. LM Studio, llama.cpp, Ollama)">
If your local LLM server 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.
@@ -146,6 +132,7 @@ 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
-18
View File
@@ -74,24 +74,6 @@ If no condenser configuration is specified, the 'noop' condenser will be used by
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.
+2 -7
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,7 +62,6 @@ 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']
@@ -255,19 +254,15 @@ 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=ENABLE_LLM_EDITOR,
enable_llm_editor=False,
enable_mcp=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
@@ -4,6 +4,7 @@ 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
@@ -18,7 +19,7 @@ describe("Empty state", () => {
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
useWsClient: vi.fn(() => ({
send: sendMock,
status: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
})),
}));
@@ -63,7 +64,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: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
}));
const user = userEvent.setup();
@@ -86,7 +87,7 @@ describe("Empty state", () => {
async () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
}));
const user = userEvent.setup();
@@ -100,7 +101,7 @@ describe("Empty state", () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
}));
rerender(<ChatInterface />);
@@ -478,7 +478,7 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
conversationStatus="RUNNING"
status="RUNNING"
/>,
);
@@ -48,7 +48,6 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -61,7 +60,6 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -74,7 +72,6 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -161,7 +158,6 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -174,7 +170,6 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -187,7 +182,6 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -65,7 +65,6 @@ describe("WsClientProvider", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "RUNNING" as const,
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
}}},
@@ -334,7 +334,10 @@ describe("Settings 404", () => {
renderHomeScreen();
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
// small hack to wait for the modal to not appear
await expect(
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
).rejects.toThrow();
});
});
-18
View File
@@ -1,18 +0,0 @@
import { heroui } from "@heroui/react";
export default heroui({
defaultTheme: "dark",
layout: {
radius: {
small: "5px",
large: "20px",
},
},
themes: {
dark: {
colors: {
primary: "#4465DB",
},
},
},
});
+2924 -3606
View File
File diff suppressed because it is too large Load Diff
+24 -25
View File
@@ -1,39 +1,37 @@
{
"name": "openhands-frontend",
"version": "0.42.0",
"version": "0.41.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "^2.8.0-beta.7",
"@heroui/react": "2.7.8",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.6.2",
"@react-router/serve": "^7.6.2",
"@react-router/node": "^7.6.1",
"@react-router/serve": "^7.6.1",
"@react-types/shared": "^3.29.1",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.80.7",
"@vitejs/plugin-react": "^4.5.2",
"@stripe/stripe-js": "^7.3.0",
"@tanstack/react-query": "^5.77.2",
"@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.9.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.17.3",
"framer-motion": "^12.14.0",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.28",
"jose": "^6.0.11",
"lucide-react": "^0.514.0",
"lucide-react": "^0.511.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.251.0",
"posthog-js": "^1.245.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -42,15 +40,15 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.6.2",
"react-router": "^7.6.1",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"tailwind-merge": "^3.3.0",
"vite": "^6.3.5",
"web-vitals": "^5.0.3",
"web-vitals": "^5.0.1",
"ws": "^8.18.2"
},
"scripts": {
@@ -84,23 +82,23 @@
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.53.0",
"@react-router/dev": "^7.6.2",
"@playwright/test": "^1.52.0",
"@react-router/dev": "^7.6.1",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.0.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/node": "^22.15.21",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^3.2.3",
"@vitest/coverage-v8": "^3.1.4",
"autoprefixer": "^10.4.21",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
@@ -115,11 +113,12 @@
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.0",
"lint-staged": "^16.0.0",
"msw": "^2.6.6",
"postcss": "^8.5.2",
"prettier": "^3.5.3",
"stripe": "^18.2.1",
"tailwindcss": "^4.1.8",
"stripe": "^18.1.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
+3 -2
View File
@@ -1,5 +1,6 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
tailwindcss: {},
autoprefixer: {},
},
};
}
@@ -117,9 +117,6 @@ const EXCLUDED_TECHNICAL_STRINGS = [
"edit-secret-form", // Test ID for secret form
"search-api-key-input", // Input name for search API key
"noopener,noreferrer", // Options for window.open
"STATUS$READY",
"STATUS$STOPPED",
"STATUS$ERROR",
];
function isExcludedTechnicalString(str) {
-31
View File
@@ -11,8 +11,6 @@ import {
GetTrajectoryResponse,
GitChangeDiff,
GitChange,
GetMicroagentsResponse,
GetMicroagentPromptResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
@@ -395,35 +393,6 @@ class OpenHands {
return data;
}
/**
* Get the available microagents associated with a conversation
* @param conversationId The ID of the conversation
* @returns The available microagents associated with the conversation
*/
static async getMicroagents(
conversationId: string,
): Promise<GetMicroagentsResponse> {
const url = `${this.getConversationUrl(conversationId)}/microagents`;
const { data } = await openHands.get<GetMicroagentsResponse>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
static async getMicroagentPrompt(
conversationId: string,
eventId: number,
): Promise<string> {
const { data } = await openHands.get<GetMicroagentPromptResponse>(
`/api/conversations/${conversationId}/remember_prompt`,
{
params: { event_id: eventId },
},
);
return data.prompt;
}
}
export default OpenHands;
+2 -25
View File
@@ -1,5 +1,4 @@
import { ConversationStatus } from "#/types/conversation-status";
import { RuntimeStatus } from "#/types/runtime-status";
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
export interface ErrorResponse {
error: string;
@@ -81,8 +80,7 @@ export interface Conversation {
git_provider: string | null;
last_updated_at: string;
created_at: string;
status: ConversationStatus;
runtime_status: RuntimeStatus | null;
status: ProjectStatus;
trigger?: ConversationTrigger;
url: string | null;
session_api_key: string | null;
@@ -104,24 +102,3 @@ export interface GitChangeDiff {
modified: string;
original: string;
}
export interface InputMetadata {
name: string;
description: string;
}
export interface Microagent {
name: string;
type: "repo" | "knowledge";
content: string;
triggers: string[];
}
export interface GetMicroagentsResponse {
microagents: Microagent[];
}
export interface GetMicroagentPromptResponse {
status: string;
prompt: string;
}
-6
View File
@@ -1,6 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

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

After

Width:  |  Height:  |  Size: 433 B

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

After

Width:  |  Height:  |  Size: 726 B

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

After

Width:  |  Height:  |  Size: 1008 B

@@ -101,7 +101,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
name="email"
type="email"
placeholder={t(I18nKey.FEEDBACK$EMAIL_PLACEHOLDER)}
className="bg-[#27272A] px-3 py-[10px] rounded-sm"
className="bg-[#27272A] px-3 py-[10px] rounded"
/>
</label>
@@ -6,7 +6,7 @@ export function BranchErrorState() {
return (
<div
data-testid="branch-dropdown-error"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm text-red-500"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
>
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_BRANCHES")}</span>
</div>
@@ -7,7 +7,7 @@ export function BranchLoadingState() {
return (
<div
data-testid="branch-dropdown-loading"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
>
<Spinner size="sm" />
<span className="text-sm">{t("HOME$LOADING_BRANCHES")}</span>
@@ -6,7 +6,7 @@ export function RepositoryErrorState() {
return (
<div
data-testid="repo-dropdown-error"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm text-red-500"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
>
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_REPOSITORIES")}</span>
</div>
@@ -7,7 +7,7 @@ export function RepositoryLoadingState() {
return (
<div
data-testid="repo-dropdown-loading"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded-sm"
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
>
<Spinner size="sm" />
<span className="text-sm">{t("HOME$LOADING_REPOSITORIES")}</span>
@@ -12,7 +12,7 @@ export function Thumbnail({ src, size = "small" }: ThumbnailProps) {
alt=""
src={src}
className={cn(
"rounded-sm object-cover",
"rounded object-cover",
size === "small" && "w-[62px] h-[62px]",
size === "large" && "w-[100px] h-[100px]",
)}
@@ -9,7 +9,6 @@ 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();
@@ -43,7 +42,7 @@ export function PaymentForm() {
>
<div
className={cn(
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded-sm px-3 py-2",
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded px-3 py-2",
"text-[28px] leading-8 -tracking-[0.02em] font-bold",
)}
>
@@ -80,7 +79,6 @@ export function PaymentForm() {
{t(I18nKey.PAYMENT$ADD_CREDIT)}
</BrandButton>
{isPending && <LoadingSpinner size="small" />}
<PoweredByStripeTag />
</div>
</div>
</form>
@@ -1,16 +0,0 @@
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-sm disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
"w-fit p-2 text-sm rounded disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
variant === "primary" && "bg-primary text-[#0D0F11]",
variant === "secondary" && "border border-primary text-primary",
variant === "danger" && "bg-red-600 text-white hover:bg-red-700",
@@ -69,7 +69,7 @@ export function MCPJsonEditor({ mcpConfig, onChange }: MCPJsonEditorProps) {
{t(I18nKey.SETTINGS$MCP_CONFIG_DESCRIPTION)}
</div>
<textarea
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-hidden"
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-none"
value={configText}
onChange={handleTextChange}
spellCheck="false"
@@ -158,7 +158,7 @@ export function SecretForm({
required
className={cn(
"resize-none",
"bg-tertiary border border-[#717888] rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
rows={8}
@@ -177,7 +177,7 @@ export function SecretForm({
defaultValue={secretDescription}
className={cn(
"resize-none",
"bg-tertiary border border-[#717888] rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
/>
@@ -63,7 +63,7 @@ export function SettingsDropdownInput({
inputProps={{
classNames: {
inputWrapper:
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
},
}}
defaultFilter={defaultFilter}
@@ -62,7 +62,7 @@ export function SettingsInput({
required={required}
pattern={pattern}
className={cn(
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
/>
@@ -16,7 +16,7 @@ export function ReplaySuggestionBox({ onChange }: ReplaySuggestionBoxProps) {
htmlFor="import-trajectory"
className="w-full flex justify-center"
>
<span className="border-2 border-dashed border-neutral-600 rounded-sm px-2 py-1 cursor-pointer">
<span className="border-2 border-dashed border-neutral-600 rounded px-2 py-1 cursor-pointer">
{t(I18nKey.LANDING$UPLOAD_TRAJECTORY)}
</span>
<input
@@ -18,7 +18,7 @@ export function EditorActionButton({
onClick={onClick}
disabled={disabled}
className={cn(
"text-sm py-0.5 rounded-sm w-20",
"text-sm py-0.5 rounded w-20",
"hover:bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
@@ -31,7 +31,7 @@ export function ModalButton({
disabled={disabled}
onClick={onClick}
className={clsx(
variant === "default" && "text-sm font-[500] py-[10px] rounded-sm",
variant === "default" && "text-sm font-[500] py-[10px] rounded",
variant === "text-like" && "text-xs leading-4 font-normal",
icon && "flex items-center justify-center gap-2",
disabled && "opacity-50 cursor-not-allowed",
@@ -36,7 +36,7 @@ export function CustomInput({
required={required}
defaultValue={defaultValue}
type={type}
className="bg-[#27272A] text-xs py-[10px] px-3 rounded-sm"
className="bg-[#27272A] text-xs py-[10px] px-3 rounded"
/>
</label>
);
@@ -16,7 +16,7 @@ export function ErrorToast({ id, error }: ErrorToastProps) {
<button
type="button"
onClick={() => toast.dismiss(id)}
className="bg-neutral-500 px-1 rounded-sm h-full"
className="bg-neutral-500 px-1 rounded h-full"
>
{t(I18nKey.ERROR_TOAST$CLOSE_BUTTON_LABEL)}
</button>
@@ -269,19 +269,19 @@ function SecurityInvariant() {
<hr className="border-t border-neutral-600 my-2" />
<ul className="space-y-2">
<div
className={`cursor-pointer p-2 rounded-sm ${activeSection === "logs" && "bg-neutral-600"}`}
className={`cursor-pointer p-2 rounded ${activeSection === "logs" && "bg-neutral-600"}`}
onClick={() => setActiveSection("logs")}
>
{t(I18nKey.INVARIANT$LOG_LABEL)}
</div>
<div
className={`cursor-pointer p-2 rounded-sm ${activeSection === "policy" && "bg-neutral-600"}`}
className={`cursor-pointer p-2 rounded ${activeSection === "policy" && "bg-neutral-600"}`}
onClick={() => setActiveSection("policy")}
>
{t(I18nKey.INVARIANT$POLICY_LABEL)}
</div>
<div
className={`cursor-pointer p-2 rounded-sm ${activeSection === "settings" && "bg-neutral-600"}`}
className={`cursor-pointer p-2 rounded ${activeSection === "settings" && "bg-neutral-600"}`}
onClick={() => setActiveSection("settings")}
>
{t(I18nKey.INVARIANT$SETTINGS_LABEL)}
@@ -92,7 +92,7 @@ export function ModelSelector({
inputProps={{
classNames: {
inputWrapper:
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
},
}}
>
@@ -142,7 +142,7 @@ export function ModelSelector({
inputProps={{
classNames: {
inputWrapper:
"bg-tertiary border border-[#717888] h-10 w-full rounded-sm p-2 placeholder:italic",
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
},
}}
>
+22 -27
View File
@@ -29,8 +29,6 @@ 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 &&
@@ -69,8 +67,14 @@ const isMessageAction = (
): event is UserMessageAction | AssistantMessageAction =>
isUserMessage(event) || isAssistantMessage(event);
export enum WsClientProviderStatus {
CONNECTED,
DISCONNECTED,
CONNECTING,
}
interface UseWsClient {
webSocketStatus: WebSocketStatus;
status: WsClientProviderStatus;
isLoadingMessages: boolean;
events: Record<string, unknown>[];
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
@@ -78,7 +82,7 @@ interface UseWsClient {
}
const WsClientContext = React.createContext<UseWsClient>({
webSocketStatus: "DISCONNECTED",
status: WsClientProviderStatus.DISCONNECTED,
isLoadingMessages: true,
events: [],
parsedEvents: [],
@@ -135,8 +139,9 @@ export function WsClientProvider({
const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
const queryClient = useQueryClient();
const sioRef = React.useRef<Socket | null>(null);
const [webSocketStatus, setWebSocketStatus] =
React.useState<WebSocketStatus>("DISCONNECTED");
const [status, setStatus] = React.useState(
WsClientProviderStatus.DISCONNECTED,
);
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const [parsedEvents, setParsedEvents] = React.useState<
(OpenHandsAction | OpenHandsObservation)[]
@@ -157,7 +162,7 @@ export function WsClientProvider({
}
function handleConnect() {
setWebSocketStatus("CONNECTED");
setStatus(WsClientProviderStatus.CONNECTED);
removeErrorMessage();
}
@@ -256,7 +261,7 @@ export function WsClientProvider({
}
function handleDisconnect(data: unknown) {
setWebSocketStatus("DISCONNECTED");
setStatus(WsClientProviderStatus.DISCONNECTED);
const sio = sioRef.current;
if (!sio) {
return;
@@ -270,7 +275,7 @@ export function WsClientProvider({
function handleError(data: unknown) {
// set status
setWebSocketStatus("DISCONNECTED");
setStatus(WsClientProviderStatus.DISCONNECTED);
updateStatusWhenErrorMessagePresent(data);
setErrorMessage(
@@ -289,14 +294,17 @@ export function WsClientProvider({
// reset events when conversationId changes
setEvents([]);
setParsedEvents([]);
setWebSocketStatus("CONNECTING");
setStatus(WsClientProviderStatus.DISCONNECTED);
}, [conversationId]);
React.useEffect(() => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}
if (conversation?.status !== "RUNNING" && !conversation?.runtime_status) {
if (
!conversation ||
["STOPPED", "STARTING"].includes(conversation.status)
) {
return () => undefined; // conversation not yet loaded
}
@@ -306,9 +314,6 @@ export function WsClientProvider({
sio.disconnect();
}
// Set initial status...
setWebSocketStatus("CONNECTING");
const lastEvent = lastEventRef.current;
const query = {
latest_event_id: lastEvent?.id ?? -1,
@@ -343,12 +348,7 @@ export function WsClientProvider({
sio.off("connect_failed", handleError);
sio.off("disconnect", handleDisconnect);
};
}, [
conversationId,
conversation?.url,
conversation?.status,
conversation?.runtime_status,
]);
}, [conversationId, conversation?.url, conversation?.status]);
React.useEffect(
() => () => {
@@ -363,18 +363,13 @@ export function WsClientProvider({
const value = React.useMemo<UseWsClient>(
() => ({
webSocketStatus,
status,
isLoadingMessages: messageRateHandler.isUnderThreshold,
events,
parsedEvents,
send,
}),
[
webSocketStatus,
messageRateHandler.isUnderThreshold,
events,
parsedEvents,
],
[status, messageRateHandler.isUnderThreshold, events, parsedEvents],
);
return <WsClientContext value={value}>{children}</WsClientContext>;
@@ -1,12 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useConversationId } from "#/hooks/use-conversation-id";
import OpenHands from "#/api/open-hands";
import { useRuntimeIsReady } from "../use-runtime-is-ready";
export const useConversationConfig = () => {
const { status } = useWsClient();
const { conversationId } = useConversationId();
const runtimeIsReady = useRuntimeIsReady();
const query = useQuery({
queryKey: ["conversation_config", conversationId],
@@ -14,7 +17,7 @@ export const useConversationConfig = () => {
if (!conversationId) throw new Error("No conversation ID");
return OpenHands.getRuntimeId(conversationId);
},
enabled: runtimeIsReady && !!conversationId,
enabled: status !== WsClientProviderStatus.DISCONNECTED && !!conversationId,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
@@ -1,25 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
interface UseConversationMicroagentsOptions {
conversationId: string | undefined;
enabled?: boolean;
}
export const useConversationMicroagents = ({
conversationId,
enabled = true,
}: UseConversationMicroagentsOptions) =>
useQuery({
queryKey: ["conversation", conversationId, "microagents"],
queryFn: async () => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}
const data = await OpenHands.getMicroagents(conversationId);
return data.microagents;
},
enabled: !!conversationId && enabled,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
@@ -1,12 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useConversationId } from "../use-conversation-id";
import OpenHands from "#/api/open-hands";
export const useGetMicroagentPrompt = ({ eventId }: { eventId: number }) => {
const { conversationId } = useConversationId();
return useQuery({
queryKey: ["conversation", "remember_prompt", conversationId, eventId],
queryFn: () => OpenHands.getMicroagentPrompt(conversationId, eventId),
});
};
+4 -16
View File
@@ -248,9 +248,6 @@ export enum I18nKey {
INVARIANT$TRACE_EXPORTED_MESSAGE = "INVARIANT$TRACE_EXPORTED_MESSAGE",
INVARIANT$POLICY_UPDATED_MESSAGE = "INVARIANT$POLICY_UPDATED_MESSAGE",
INVARIANT$SETTINGS_UPDATED_MESSAGE = "INVARIANT$SETTINGS_UPDATED_MESSAGE",
CHAT_INTERFACE$DISCONNECTED = "CHAT_INTERFACE$DISCONNECTED",
CHAT_INTERFACE$CONNECTING = "CHAT_INTERFACE$CONNECTING",
CHAT_INTERFACE$STOPPED = "CHAT_INTERFACE$STOPPED",
CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE = "CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE",
CHAT_INTERFACE$AGENT_INIT_MESSAGE = "CHAT_INTERFACE$AGENT_INIT_MESSAGE",
CHAT_INTERFACE$AGENT_RUNNING_MESSAGE = "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE",
@@ -369,8 +366,10 @@ export enum I18nKey {
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL",
SIDEBAR$CONVERSATIONS = "SIDEBAR$CONVERSATIONS",
STATUS$CONNECTING_TO_RUNTIME = "STATUS$CONNECTING_TO_RUNTIME",
STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME",
STATUS$STARTING_CONTAINER = "STATUS$STARTING_CONTAINER",
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
STATUS$CONTAINER_STARTED = "STATUS$CONTAINER_STARTED",
STATUS$SETTING_UP_WORKSPACE = "STATUS$SETTING_UP_WORKSPACE",
STATUS$SETTING_UP_GIT_HOOKS = "STATUS$SETTING_UP_GIT_HOOKS",
ACCOUNT_SETTINGS_MODAL$DISCONNECT = "ACCOUNT_SETTINGS_MODAL$DISCONNECT",
@@ -461,7 +460,7 @@ export enum I18nKey {
CONVERSATION$NO_CONVERSATIONS = "CONVERSATION$NO_CONVERSATIONS",
LANDING$SELECT_GIT_REPO = "LANDING$SELECT_GIT_REPO",
BUTTON$SEND = "BUTTON$SEND",
STATUS$BUILDING_RUNTIME = "STATUS$BUILDING_RUNTIME",
STATUS$WAITING_FOR_CLIENT = "STATUS$WAITING_FOR_CLIENT",
SUGGESTIONS$WHAT_TO_BUILD = "SUGGESTIONS$WHAT_TO_BUILD",
SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL = "SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL",
BUTTON$MARK_HELPFUL = "BUTTON$MARK_HELPFUL",
@@ -471,7 +470,6 @@ export enum I18nKey {
BILLING$YOUVE_GOT_50 = "BILLING$YOUVE_GOT_50",
BILLING$ERROR_WHILE_CREATING_SESSION = "BILLING$ERROR_WHILE_CREATING_SESSION",
BILLING$CLAIM_YOUR_50 = "BILLING$CLAIM_YOUR_50",
BILLING$POWERED_BY = "BILLING$POWERED_BY",
BILLING$PROCEED_TO_STRIPE = "BILLING$PROCEED_TO_STRIPE",
BILLING$YOURE_IN = "BILLING$YOURE_IN",
PAYMENT$ADD_FUNDS = "PAYMENT$ADD_FUNDS",
@@ -545,16 +543,6 @@ export enum I18nKey {
TOS$CONTINUE = "TOS$CONTINUE",
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
TIPS$CUSTOMIZE_MICROAGENT = "TIPS$CUSTOMIZE_MICROAGENT",
CONVERSATION$SHOW_MICROAGENTS = "CONVERSATION$SHOW_MICROAGENTS",
CONVERSATION$NO_MICROAGENTS = "CONVERSATION$NO_MICROAGENTS",
CONVERSATION$FAILED_TO_FETCH_MICROAGENTS = "CONVERSATION$FAILED_TO_FETCH_MICROAGENTS",
MICROAGENTS_MODAL$TITLE = "MICROAGENTS_MODAL$TITLE",
MICROAGENTS_MODAL$TRIGGERS = "MICROAGENTS_MODAL$TRIGGERS",
MICROAGENTS_MODAL$INPUTS = "MICROAGENTS_MODAL$INPUTS",
MICROAGENTS_MODAL$TOOLS = "MICROAGENTS_MODAL$TOOLS",
MICROAGENTS_MODAL$CONTENT = "MICROAGENTS_MODAL$CONTENT",
MICROAGENTS_MODAL$NO_CONTENT = "MICROAGENTS_MODAL$NO_CONTENT",
MICROAGENTS_MODAL$FETCH_ERROR = "MICROAGENTS_MODAL$FETCH_ERROR",
TIPS$SETUP_SCRIPT = "TIPS$SETUP_SCRIPT",
TIPS$VSCODE_INSTANCE = "TIPS$VSCODE_INSTANCE",
TIPS$SAVE_WORK = "TIPS$SAVE_WORK",
+91 -283
View File
@@ -3967,69 +3967,21 @@
"ja": "設定を更新しました",
"uk": "Налаштування оновлено"
},
"CHAT_INTERFACE$DISCONNECTED": {
"en": "Disconnected",
"ja": "切断されました",
"zh-CN": "已断开连接",
"zh-TW": "已斷開連接",
"ko-KR": "연결 끊김",
"no": "Frakoblet",
"it": "Disconnesso",
"pt": "Desconectado",
"es": "Desconectado",
"ar": "تم قطع الاتصال",
"fr": "Déconnecté",
"tr": "Bağlantı kesildi",
"de": "Getrennt",
"uk": "Від'єднано"
},
"CHAT_INTERFACE$CONNECTING": {
"en": "Connecting",
"ja": "接続中",
"zh-CN": "正在连接",
"zh-TW": "正在連接",
"ko-KR": "연결 중",
"no": "Kobler til",
"it": "Connessione in corso",
"pt": "Conectando",
"es": "Conectando",
"ar": "جاري الاتصال",
"fr": "Connexion en cours",
"tr": "Bağlanıyor",
"de": "Verbindung wird hergestellt",
"uk": "З'єднання"
},
"CHAT_INTERFACE$STOPPED": {
"en": "Stopped",
"ja": "停止しました",
"zh-CN": "已停止",
"zh-TW": "已停止",
"ko-KR": "중지됨",
"no": "Stoppet",
"it": "Fermato",
"pt": "Parado",
"es": "Detenido",
"ar": "توقف",
"fr": "Arrêté",
"tr": "Durduruldu",
"de": "Angehalten",
"uk": "Зупинено"
},
"CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": {
"en": "Initializing Agent...",
"de": "Agent wird initialisiert...",
"zh-CN": "正在初始化智能体...",
"zh-TW": "正在初始化智能體...",
"ko-KR": "에이전트 초기화 중...",
"no": "Initialiserer agent...",
"it": "Inizializzazione dell'agente...",
"pt": "Inicializando o Agente...",
"es": "Inicializando Agente...",
"ar": "جارٍ تهيئة الوكيل...",
"fr": "Initialisation de l'agent...",
"tr": "Ajan başlatılıyor...",
"ja": "エージェントを初期化中...",
"uk": "Ініціалізація агента..."
"en": "Starting up!",
"de": "Wird gestartet!",
"zh-CN": "正在启动!",
"zh-TW": "正在啟動!",
"ko-KR": "시작 중입니다!",
"no": "Starter opp!",
"it": "Avvio in corso!",
"pt": "Iniciando!",
"es": "¡Iniciando!",
"ar": "جارٍ البدء!",
"fr": "Démarrage en cours !",
"tr": "Başlatılıyor!",
"ja": "起動中!",
"uk": "Запуск!"
},
"CHAT_INTERFACE$AGENT_INIT_MESSAGE": {
"en": "Agent is initialized, waiting for task...",
@@ -5903,22 +5855,6 @@
"tr": "Konuşmalar",
"uk": "Розмови"
},
"STATUS$CONNECTING_TO_RUNTIME": {
"en": "Connecting to runtime...",
"zh-CN": "正在连接到运行时...",
"zh-TW": "正在連接到執行時...",
"de": "Verbinde mit der Laufzeitumgebung...",
"ko-KR": "런타임에 연결 중...",
"no": "Kobler til kjøretidsmiljø...",
"it": "Connessione all'ambiente di esecuzione in corso...",
"pt": "Conectando ao ambiente de execução...",
"es": "Conectando al entorno de ejecución...",
"ar": "جارٍ الاتصال ببيئة التشغيل...",
"fr": "Connexion à l'environnement d'exécution en cours...",
"tr": "Çalışma zamanı ortamına bağlanılıyor...",
"ja": "ランタイムに接続中",
"uk": "Підключення до середовища виконання..."
},
"STATUS$STARTING_RUNTIME": {
"en": "Starting runtime...",
"zh-CN": "启动运行时...",
@@ -5935,6 +5871,54 @@
"ja": "ランタイムを開始中",
"uk": "Запуск середовища виконання..."
},
"STATUS$STARTING_CONTAINER": {
"en": "Preparing container, this might take a few minutes...",
"zh-CN": "正在准备容器,这可能需要几分钟...",
"zh-TW": "正在準備容器,這可能需要幾分鐘...",
"de": "Container wird vorbereitet, dies kann einige Minuten dauern...",
"ko-KR": "컨테이너를 준비 중입니다. 몇 분 정도 걸릴 수 있습니다...",
"no": "Forbereder container, dette kan ta noen minutter...",
"it": "Preparazione del container in corso, potrebbe richiedere alcuni minuti...",
"pt": "Preparando o container, isso pode levar alguns minutos...",
"es": "Preparando el contenedor, esto puede tardar unos minutos...",
"ar": "جارٍ إعداد الحاوية، قد يستغرق هذا بضع دقائق...",
"fr": "Préparation du conteneur, cela peut prendre quelques minutes...",
"tr": "Konteyner hazırlanıyor, bu işlem birkaç dakika sürebilir...",
"ja": "コンテナを開始中",
"uk": "Підготовка контейнера, це може тривати кілька хвилин..."
},
"STATUS$PREPARING_CONTAINER": {
"en": "Preparing to start container...",
"zh-CN": "正在准备启动容器...",
"zh-TW": "正在準備啟動容器...",
"de": "Vorbereitung zum Starten des Containers...",
"ko-KR": "컨테이너 시작 준비 중...",
"no": "Forbereder å starte container...",
"it": "Preparazione all'avvio del container...",
"pt": "Preparando para iniciar o container...",
"es": "Preparando para iniciar el contenedor...",
"ar": "جارٍ التحضير لبدء الحاوية...",
"fr": "Préparation du démarrage du conteneur...",
"tr": "Konteyner başlatılmaya hazırlanıyor...",
"ja": "コンテナを準備中",
"uk": "Preparing to start container..."
},
"STATUS$CONTAINER_STARTED": {
"en": "Container started.",
"zh-CN": "容器已启动。",
"zh-TW": "容器已啟動。",
"de": "Container gestartet.",
"ko-KR": "컨테이너가 시작되었습니다.",
"no": "Container startet.",
"it": "Container avviato.",
"pt": "Container iniciado.",
"es": "Contenedor iniciado.",
"ar": "تم بدء الحاوية.",
"fr": "Conteneur démarré.",
"tr": "Konteyner başlatıldı.",
"ja": "コンテナが開始されました",
"uk": "Контейнер запущено."
},
"STATUS$SETTING_UP_WORKSPACE": {
"en": "Setting up workspace...",
"zh-CN": "正在设置工作区...",
@@ -7375,21 +7359,21 @@
"tr": "Gönder",
"uk": "Надіслати"
},
"STATUS$BUILDING_RUNTIME": {
"en": "Building Runtime...",
"ja": "ランタイムを構築中...",
"zh-CN": "正在构建运行时...",
"zh-TW": "正在構建運行時...",
"ko-KR": "런타임 구축 중...",
"no": "Bygger kjøretidsmiljø...",
"it": "Costruzione del runtime in corso...",
"pt": "Construindo ambiente de execução...",
"es": "Construyendo entorno de ejecución...",
"ar": "جاري بناء بيئة التشغيل...",
"fr": "Construction de l'environnement d'exécution...",
"tr": "Çalışma ortamı oluşturuluyor...",
"de": "Laufzeitumgebung wird erstellt...",
"uk": "Створення середовища виконання..."
"STATUS$WAITING_FOR_CLIENT": {
"en": "Waiting for client to become ready...",
"zh-CN": "等待客户端准备就绪...",
"zh-TW": "等待客戶端準備就緒...",
"de": "Warten auf Bereitschaft des Clients...",
"ko-KR": "클라이언트가 준비될 때까지 기다리는 중...",
"no": "Venter på at klienten skal bli klar...",
"it": "In attesa che il client sia pronto...",
"pt": "Aguardando o cliente ficar pronto...",
"es": "Esperando a que el cliente esté listo...",
"ar": "في انتظار جاهزية العميل...",
"fr": "En attente que le client soit prêt...",
"tr": "İstemcinin hazır olması bekleniyor...",
"ja": "クライアントの準備を待機中",
"uk": "Чекаємо на готовність клієнта..."
},
"SUGGESTIONS$WHAT_TO_BUILD": {
"en": "What do you want to build?",
@@ -7535,22 +7519,6 @@
"de": "Fügen Sie eine Kreditkarte mit Stripe hinzu, um $50 zu erhalten. <b>Wir belasten Sie nicht ohne vorherige Zustimmung!</b>",
"uk": "Додайте кредитну картку до Stripe, щоб отримати свої 50 доларів. <b>Ми не стягуватимемо з вас плату без попереднього запиту!</b>"
},
"BILLING$POWERED_BY": {
"en": "Powered by",
"ja": "提供:",
"zh-CN": "技术支持:",
"zh-TW": "技術支援:",
"ko-KR": "제공: ",
"no": "Drevet av",
"it": "Offerto da",
"pt": "Oferecido por",
"es": "Ofrecido por",
"ar": "مشغل بواسطة",
"fr": "Propulsé par",
"tr": "Tarafından desteklenmektedir",
"de": "Bereitgestellt von",
"uk": "Працює на базі"
},
"BILLING$PROCEED_TO_STRIPE": {
"en": "Add Billing Info",
"ja": "請求情報を追加",
@@ -8704,180 +8672,20 @@
"tr": "Hizmet Şartlarını kabul ederken hata oluştu"
},
"TIPS$CUSTOMIZE_MICROAGENT": {
"en": "You can customize OpenHands for your repo using an available microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",
"ja": "利用可能なマイクロエージェントを使用して、リポジトリ用にOpenHandsをカスタマイズできます。OpenHandsに、コードの実行方法を含むリポジトリの説明を.openhands/microagents/repo.mdに入れるよう依頼してください。",
"zh-CN": "您可以使用可用微代理为您的仓库自定义OpenHands。请OpenHands将仓库的描述(包括如何运行代码)放入.openhands/microagents/repo.md。",
"zh-TW": "您可以使用可用微代理為您的倉庫自定義OpenHands。請OpenHands將倉庫的描述(包括如何運行代碼)放入.openhands/microagents/repo.md。",
"ko-KR": "사용 가능한 마이크로에이전트를 사용하여 저장소에 맞게 OpenHands를 사용자 정의할 수 있습니다. OpenHands에게 코드 실행 방법을 포함한 저장소 설명을 .openhands/microagents/repo.md에 넣도록 요청하세요.",
"no": "Du kan tilpasse OpenHands for ditt repo ved å bruke en tilgjengelig mikroagent. Be OpenHands om å legge en beskrivelse av repoet, inkludert hvordan du kjører koden, i .openhands/microagents/repo.md.",
"it": "Puoi personalizzare OpenHands per il tuo repository utilizzando un microagente disponibile. Chiedi a OpenHands di inserire una descrizione del repository, incluso come eseguire il codice, in .openhands/microagents/repo.md.",
"pt": "Você pode personalizar o OpenHands para seu repositório usando um microagente disponível. Peça ao OpenHands para colocar uma descrição do repositório, incluindo como executar o código, em .openhands/microagents/repo.md.",
"es": "Puede personalizar OpenHands para su repositorio utilizando un microagente disponible. Pídale a OpenHands que ponga una descripción del repositorio, incluido cómo ejecutar el código, en .openhands/microagents/repo.md.",
"ar": "يمكنك تخصيص OpenHands لمستودعك باستخدام وكيل مصغر متاح. اطلب من OpenHands وضع وصف للمستودع، بما في ذلك كيفية تشغيل الكود، في .openhands/microagents/repo.md.",
"de": "Sie können OpenHands für Ihr Repository mit einem verfügbaren Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren.",
"fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent disponible. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.",
"tr": "Kullanılabilir bir mikro ajan kullanarak OpenHands'i deponuz için özelleştirebilirsiniz. OpenHands'ten deponun açıklamasını, kodun nasıl çalıştırılacağı dahil, .openhands/microagents/repo.md dosyasına koymasını isteyin.",
"uk": "Ви можете налаштувати OpenHands для свого репозиторію за допомогою доступного мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інформацію про те, як запустити код, у файлі .openhands/microagents/repo.md."
},
"CONVERSATION$SHOW_MICROAGENTS": {
"en": "Show Available Microagents",
"ja": "利用可能なマイクロエージェントを表示",
"zh-CN": "显示可用微代理",
"zh-TW": "顯示可用微代理",
"ko-KR": "사용 가능한 마이크로에이전트 표시",
"no": "Vis tilgjengelige mikroagenter",
"ar": "عرض الوكلاء المصغرين المتاحة",
"de": "Verfügbare Mikroagenten anzeigen",
"fr": "Afficher les micro-agents disponibles",
"it": "Mostra microagenti disponibili",
"pt": "Mostrar microagentes disponíveis",
"es": "Mostrar microagentes disponibles",
"tr": "Kullanılabilir mikro ajanları göster",
"uk": "Показати доступних мікроагентів"
},
"CONVERSATION$NO_MICROAGENTS": {
"en": "No available microagents found for this conversation.",
"ja": "この会話用の利用可能なマイクロエージェントが見つかりませんでした。",
"zh-CN": "未找到此对话的可用微代理。",
"zh-TW": "未找到此對話的可用微代理。",
"ko-KR": "이 대화에 대한 사용 가능한 마이크로에이전트를 찾을 수 없습니다.",
"no": "Ingen tilgjengelige mikroagenter funnet for denne samtalen.",
"ar": "لم يتم العثور على وكلاء مصغرين متاحة لهذه المحادثة.",
"de": "Keine verfügbaren Mikroagenten für dieses Gespräch gefunden.",
"fr": "Aucun micro-agent disponible trouvé pour cette conversation.",
"it": "Nessun microagente disponibile trovato per questa conversazione.",
"pt": "Nenhum microagente disponível encontrado para esta conversa.",
"es": "No se encontraron microagentes disponibles para esta conversación.",
"tr": "Bu konuşma için kullanılabilir mikro ajan bulunamadı.",
"uk": "Для цієї розмови не знайдено доступних мікроагентів."
},
"CONVERSATION$FAILED_TO_FETCH_MICROAGENTS": {
"en": "Failed to fetch available microagents",
"ja": "利用可能なマイクロエージェントの取得に失敗しました",
"zh-CN": "获取可用微代理失败",
"zh-TW": "獲取可用微代理失敗",
"ko-KR": "사용 가능한 마이크로에이전트를 가져오지 못했습니다",
"no": "Kunne ikke hente tilgjengelige mikroagenter",
"ar": "فشل في جلب الوكلاء المصغرين المتاحة",
"de": "Fehler beim Abrufen von verfügbaren Mikroagenten",
"fr": "Échec de la récupération des micro-agents disponibles",
"it": "Impossibile recuperare i microagenti disponibili",
"pt": "Falha ao buscar microagentes disponíveis",
"es": "Error al obtener microagentes disponibles",
"tr": "Kullanılabilir mikro ajanlar getirilemedi",
"uk": "Не вдалося отримати доступних мікроагентів"
},
"MICROAGENTS_MODAL$TITLE": {
"en": "Available Microagents",
"ja": "利用可能なマイクロエージェント",
"zh-CN": "可用微代理",
"zh-TW": "可用微代理",
"ko-KR": "사용 가능한 마이크로에이전트",
"no": "Tilgjengelige mikroagenter",
"ar": "الوكلاء المصغرين المتاحة",
"de": "Verfügbare Mikroagenten",
"fr": "Micro-agents disponibles",
"it": "Microagenti disponibili",
"pt": "Microagentes disponíveis",
"es": "Microagentes disponibles",
"tr": "Kullanılabilir mikro ajanlar",
"uk": "Доступні мікроагенти"
},
"MICROAGENTS_MODAL$TRIGGERS": {
"en": "Triggers",
"ja": "トリガー",
"zh-CN": "触发器",
"zh-TW": "觸發器",
"ko-KR": "트리거",
"no": "Utløsere",
"ar": "المحفزات",
"de": "Auslöser",
"fr": "Déclencheurs",
"it": "Trigger",
"pt": "Gatilhos",
"es": "Disparadores",
"tr": "Tetikleyiciler",
"uk": "Тригери"
},
"MICROAGENTS_MODAL$INPUTS": {
"en": "Inputs",
"ja": "入力",
"zh-CN": "输入",
"zh-TW": "輸入",
"ko-KR": "입력",
"no": "Inndata",
"ar": "المدخلات",
"de": "Eingaben",
"fr": "Entrées",
"it": "Input",
"pt": "Entradas",
"es": "Entradas",
"tr": "Girdiler",
"uk": "Вхідні дані"
},
"MICROAGENTS_MODAL$TOOLS": {
"en": "Tools",
"ja": "ツール",
"zh-CN": "工具",
"zh-TW": "工具",
"ko-KR": "도구",
"no": "Verktøy",
"ar": "الأدوات",
"de": "Werkzeuge",
"fr": "Outils",
"it": "Strumenti",
"pt": "Ferramentas",
"es": "Herramientas",
"tr": "Araçlar",
"uk": "Інструменти"
},
"MICROAGENTS_MODAL$CONTENT": {
"en": "Content",
"ja": "コンテンツ",
"zh-CN": "内容",
"zh-TW": "內容",
"ko-KR": "콘텐츠",
"no": "Innhold",
"ar": "المحتوى",
"de": "Inhalt",
"fr": "Contenu",
"it": "Contenuto",
"pt": "Conteúdo",
"es": "Contenido",
"tr": "İçerik",
"uk": "Вміст"
},
"MICROAGENTS_MODAL$NO_CONTENT": {
"en": "Microagent has no content",
"ja": "マイクロエージェントにコンテンツがありません",
"zh-CN": "微代理没有内容",
"zh-TW": "微代理沒有內容",
"ko-KR": "마이크로에이전트에 콘텐츠가 없습니다",
"no": "Mikroagenten har ikke innhold",
"ar": "الوكيل المصغر ليس لديه محتوى",
"de": "Mikroagent hat keinen Inhalt",
"fr": "Le micro-agent n'a pas de contenu",
"it": "Il microagente non ha contenuto",
"pt": "Microagente não tem conteúdo",
"es": "El microagente no tiene contenido",
"tr": "Mikroajanın içeriği yok",
"uk": "Мікроагент не має вмісту"
},
"MICROAGENTS_MODAL$FETCH_ERROR": {
"en": "Failed to fetch microagents. Please try again later.",
"ja": "マイクロエージェントの取得に失敗しました。後でもう一度お試しください。",
"zh-CN": "获取微代理失败。请稍后再试。",
"zh-TW": "獲取微代理失敗。請稍後再試。",
"ko-KR": "마이크로에이전트를 가져오지 못했습니다. 나중에 다시 시도해 주세요.",
"no": "Kunne ikke hente mikroagenter. Prøv igjen senere.",
"ar": "فشل في جلب الوكلاء المصغرين. يرجى المحاولة مرة أخرى لاحقًا.",
"de": "Mikroagenten konnten nicht abgerufen werden. Bitte versuchen Sie es später erneut.",
"fr": "Échec de la récupération des micro-agents. Veuillez réessayer plus tard.",
"it": "Impossibile recuperare i microagenti. Riprova più tardi.",
"pt": "Falha ao buscar microagentes. Por favor, tente novamente mais tarde.",
"es": "Error al obtener microagentes. Por favor, inténtelo de nuevo más tarde.",
"tr": "Mikroajanlar getirilemedi. Lütfen daha sonra tekrar deneyin.",
"uk": "Не вдалося отримати мікроагентів. Будь ласка, спробуйте пізніше."
"en": "You can customize OpenHands for your repo using a microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",
"ja": "マイクロエージェントを使用して、リポジトリ用にOpenHandsをカスタマイズできます。OpenHandsに、コードの実行方法を含むリポジトリの説明を.openhands/microagents/repo.mdに入れるよう依頼してください。",
"zh-CN": "您可以使用微代理为您的仓库自定义OpenHands。请OpenHands将仓库的描述(包括如何运行代码)放入.openhands/microagents/repo.md。",
"zh-TW": "您可以使用微代理為您的倉庫自定義OpenHands。請OpenHands將倉庫的描述(包括如何運行代碼)放入.openhands/microagents/repo.md。",
"ko-KR": "마이크로에이전트를 사용하여 저장소에 맞게 OpenHands를 사용자 정의할 수 있습니다. OpenHands에게 코드 실행 방법을 포함한 저장소 설명을 .openhands/microagents/repo.md에 넣도록 요청하세요.",
"no": "Du kan tilpasse OpenHands for ditt repo ved å bruke en mikroagent. Be OpenHands om å legge en beskrivelse av repoet, inkludert hvordan du kjører koden, i .openhands/microagents/repo.md.",
"it": "Puoi personalizzare OpenHands per il tuo repository utilizzando un microagente. Chiedi a OpenHands di inserire una descrizione del repository, incluso come eseguire il codice, in .openhands/microagents/repo.md.",
"pt": "Você pode personalizar o OpenHands para seu repositório usando um microagente. Peça ao OpenHands para colocar uma descrição do repositório, incluindo como executar o código, em .openhands/microagents/repo.md.",
"es": "Puede personalizar OpenHands para su repositorio utilizando un microagente. Pídale a OpenHands que ponga una descripción del repositorio, incluido cómo ejecutar el código, en .openhands/microagents/repo.md.",
"ar": "يمكنك تخصيص OpenHands لمستودعك باستخدام وكيل مصغر. اطلب من OpenHands وضع وصف للمستودع، بما في ذلك كيفية تشغيل الكود، في .openhands/microagents/repo.md.",
"fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.",
"tr": "Bir mikro ajan kullanarak deponuz için OpenHands'i özelleştirebilirsiniz. OpenHands'ten kodun nasıl çalıştırılacağı da dahil olmak üzere deponun açıklamasını .openhands/microagents/repo.md dosyasına koymasını isteyin.",
"de": "Sie können OpenHands für Ihr Repository mit einem Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren.",
"uk": "Ви можете налаштувати OpenHands для вашого репозиторію за допомогою мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інструкції з запуску коду, у файлі .openhands/microagents/repo.md."
},
"TIPS$SETUP_SCRIPT": {
"en": "You can add .openhands/setup.sh to your repository to automatically run a setup script every time you start an OpenHands conversation.",
-4
View File
@@ -57,7 +57,6 @@ const conversations: Conversation[] = [
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
},
@@ -73,7 +72,6 @@ const conversations: Conversation[] = [
).toISOString(),
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
status: "STOPPED",
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -89,7 +87,6 @@ const conversations: Conversation[] = [
).toISOString(),
created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
status: "STOPPED",
runtime_status: null,
url: null,
session_api_key: null,
},
@@ -285,7 +282,6 @@ export const handlers = [
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
};
-1
View File
@@ -19,7 +19,6 @@ const chat = ws.link(`ws://${window?.location.host}/socket.io`);
export const handlers: WebSocketHandler[] = [
chat.addEventListener("connection", (connection) => {
// @ts-expect-error - MSW v2 type incompatibility
const io = toSocketIo(connection);
// @ts-expect-error - accessing private property for testing purposes
const { url }: { url: URL } = io.client.connection;
+6 -6
View File
@@ -41,11 +41,11 @@ function EmailInputSection({
type="email"
value={email}
onChange={onEmailChange}
className={`text-base text-white p-2 bg-base-tertiary rounded-sm border ${
className={`text-base text-white p-2 bg-base-tertiary rounded border ${
isEmailChanged && !isEmailValid
? "border-red-500"
: "border-tertiary"
} flex-grow focus:outline-hidden focus:border-transparent focus:ring-0`}
} flex-grow focus:outline-none focus:border-transparent focus:ring-0`}
placeholder={t("SETTINGS$USER_EMAIL_LOADING")}
data-testid="email-input"
/>
@@ -65,7 +65,7 @@ function EmailInputSection({
type="button"
onClick={onSaveEmail}
disabled={!isEmailChanged || isSaving || !isEmailValid}
className="px-4 py-2 rounded-sm bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
className="px-4 py-2 rounded bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
data-testid="save-email-button"
>
{isSaving ? t("SETTINGS$SAVING") : t("SETTINGS$SAVE")}
@@ -76,7 +76,7 @@ function EmailInputSection({
type="button"
onClick={onResendVerification}
disabled={isResendingVerification}
className="px-4 py-2 rounded-sm bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
className="px-4 py-2 rounded bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]"
data-testid="resend-verification-button"
>
{isResendingVerification
@@ -96,7 +96,7 @@ function VerificationAlert() {
const { t } = useTranslation();
return (
<div
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-sm mt-4"
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mt-4"
role="alert"
>
<p className="font-bold">{t("SETTINGS$EMAIL_VERIFICATION_REQUIRED")}</p>
@@ -205,7 +205,7 @@ function UserSettingsScreen() {
<div data-testid="user-settings-screen" className="flex flex-col h-full">
<div className="p-9 flex flex-col gap-6">
{isLoading ? (
<div className="animate-pulse h-8 w-64 bg-tertiary rounded-sm" />
<div className="animate-pulse h-8 w-64 bg-tertiary rounded" />
) : (
<EmailInputSection
email={email}
+1 -1
View File
@@ -76,7 +76,7 @@ function VSCodeTab() {
<button
type="button"
onClick={handleOpenInNewTab}
className="px-4 py-2 bg-primary text-white rounded-sm hover:bg-primary-dark transition-colors"
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark transition-colors"
>
{t("VSCODE$OPEN_IN_NEW_TAB")}
</button>
+4 -7
View File
@@ -1,12 +1,9 @@
@import "tailwindcss";
@plugin '../hero.ts';
@config "../tailwind.config.js";
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@tailwind base;
@tailwind components;
@tailwind utilities;
.button-base {
@apply bg-tertiary border border-neutral-600 rounded-xs;
@apply bg-tertiary border border-neutral-600 rounded;
}
.skeleton {
@@ -1 +0,0 @@
export type ConversationStatus = "STARTING" | "RUNNING" | "STOPPED";
-4
View File
@@ -1,7 +1,3 @@
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use types in `frontend/src/types/core` instead.
*/
export interface ActionMessage {
id: number;
-8
View File
@@ -1,8 +0,0 @@
export type RuntimeStatus =
| "STATUS$STOPPED"
| "STATUS$BUILDING_RUNTIME"
| "STATUS$STARTING_RUNTIME"
| "STATUS$RUNTIME_STARTED"
| "STATUS$SETTING_UP_WORKSPACE"
| "STATUS$SETTING_UP_GIT_HOOKS"
| "STATUS$READY";
+1 -4
View File
@@ -5,10 +5,7 @@
* @returns The URL to redirect to for OAuth
*/
export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
// Use HTTPS protocol unless the host is localhost
const protocol =
requestUrl.hostname === "localhost" ? requestUrl.protocol : "https:";
const redirectUri = `${protocol}//${requestUrl.host}/oauth/keycloak/callback`;
const redirectUri = `${requestUrl.origin}/oauth/keycloak/callback`;
let authUrl = requestUrl.hostname
.replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev")
.replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev")
-121
View File
@@ -1,121 +0,0 @@
import { WebSocketStatus } from "#/context/ws-client-provider";
import { I18nKey } from "#/i18n/declaration";
import { AgentState } from "#/types/agent-state";
import { ConversationStatus } from "#/types/conversation-status";
import { StatusMessage } from "#/types/message";
import { RuntimeStatus } from "#/types/runtime-status";
export enum IndicatorColor {
BLUE = "bg-blue-500",
GREEN = "bg-green-500",
ORANGE = "bg-orange-500",
YELLOW = "bg-yellow-500",
RED = "bg-red-500",
DARK_ORANGE = "bg-orange-800",
}
export const AGENT_STATUS_MAP: {
[k: string]: string;
} = {
[AgentState.INIT]: I18nKey.CHAT_INTERFACE$AGENT_INIT_MESSAGE,
[AgentState.RUNNING]: I18nKey.CHAT_INTERFACE$AGENT_RUNNING_MESSAGE,
[AgentState.AWAITING_USER_INPUT]:
I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE,
[AgentState.PAUSED]: I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE,
[AgentState.LOADING]:
I18nKey.CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE,
[AgentState.STOPPED]: I18nKey.CHAT_INTERFACE$AGENT_STOPPED_MESSAGE,
[AgentState.FINISHED]: I18nKey.CHAT_INTERFACE$AGENT_FINISHED_MESSAGE,
[AgentState.REJECTED]: I18nKey.CHAT_INTERFACE$AGENT_REJECTED_MESSAGE,
[AgentState.ERROR]: I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE,
[AgentState.AWAITING_USER_CONFIRMATION]:
I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE,
[AgentState.USER_CONFIRMED]:
I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE,
[AgentState.USER_REJECTED]:
I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE,
[AgentState.RATE_LIMITED]: I18nKey.CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE,
};
export function getIndicatorColor(
webSocketStatus: WebSocketStatus,
conversationStatus: ConversationStatus | null,
runtimeStatus: RuntimeStatus | null,
agentState: AgentState | null,
) {
if (
webSocketStatus === "DISCONNECTED" ||
conversationStatus === "STOPPED" ||
runtimeStatus === "STATUS$STOPPED" ||
agentState === AgentState.STOPPED
) {
return IndicatorColor.RED;
}
// Display a yellow working icon while the runtime is starting
if (
conversationStatus === "STARTING" ||
!["STATUS$READY", null].includes(runtimeStatus) ||
(agentState != null &&
[
AgentState.LOADING,
AgentState.PAUSED,
AgentState.REJECTED,
AgentState.RATE_LIMITED,
].includes(agentState))
) {
return IndicatorColor.YELLOW;
}
if (agentState === AgentState.AWAITING_USER_CONFIRMATION) {
return IndicatorColor.ORANGE;
}
if (agentState === AgentState.AWAITING_USER_INPUT) {
return IndicatorColor.BLUE;
}
// All other agent states are green
return IndicatorColor.GREEN;
}
export function getStatusCode(
statusMessage: StatusMessage,
webSocketStatus: WebSocketStatus,
conversationStatus: ConversationStatus | null,
runtimeStatus: RuntimeStatus | null,
agentState: AgentState | null,
) {
if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") {
return I18nKey.CHAT_INTERFACE$STOPPED;
}
if (runtimeStatus === "STATUS$BUILDING_RUNTIME") {
return I18nKey.STATUS$BUILDING_RUNTIME;
}
if (runtimeStatus === "STATUS$STARTING_RUNTIME") {
return I18nKey.STATUS$STARTING_RUNTIME;
}
if (webSocketStatus === "DISCONNECTED") {
return I18nKey.CHAT_INTERFACE$DISCONNECTED;
}
if (webSocketStatus === "CONNECTING") {
return I18nKey.CHAT_INTERFACE$CONNECTING;
}
if (
agentState === AgentState.LOADING &&
statusMessage?.id &&
statusMessage.id !== "STATUS$READY"
) {
return statusMessage.id;
}
if (agentState) {
return AGENT_STATUS_MAP[agentState];
}
if (runtimeStatus && runtimeStatus !== "STATUS$READY" && !agentState) {
return runtimeStatus;
}
return "STATUS$ERROR"; // illegal state
}
+24 -1
View File
@@ -2,6 +2,10 @@
import { heroui } from "@heroui/react";
import typography from "@tailwindcss/typography";
export default {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
@@ -20,5 +24,24 @@ export default {
},
},
darkMode: "class",
plugins: [typography],
plugins: [
heroui({
defaultTheme: "dark",
layout: {
radius: {
small: "5px",
large: "20px",
},
},
themes: {
dark: {
colors: {
primary: "#4465DB",
logo: "#CFB755",
},
},
},
}),
typography,
],
};
+1 -3
View File
@@ -5,7 +5,6 @@ import viteTsconfigPaths from "vite-tsconfig-paths";
import svgr from "vite-plugin-svgr";
import { reactRouter } from "@react-router/dev/vite";
import { configDefaults } from "vitest/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig(({ mode }) => {
const {
@@ -29,7 +28,6 @@ export default defineConfig(({ mode }) => {
!process.env.VITEST && reactRouter(),
viteTsconfigPaths(),
svgr(),
tailwindcss(),
],
server: {
port: FE_PORT,
@@ -56,7 +54,7 @@ export default defineConfig(({ mode }) => {
},
},
watch: {
ignored: ["**/node_modules/**", "**/.git/**"],
ignored: ['**/node_modules/**', '**/.git/**'],
},
},
ssr: {
@@ -141,9 +141,6 @@ def response_to_actions(
content=arguments['content'],
start=arguments.get('start', 1),
end=arguments.get('end', -1),
impl_source=arguments.get(
'impl_source', FileEditSource.LLM_BASED_EDIT
),
)
elif (
tool_call.function.name
@@ -2,18 +2,10 @@ from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChun
_FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format.
* The assistant can edit files by specifying the file path and providing a draft of the new file content.
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# ... existing code ...` to indicate unchanged sections.
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# unchanged` to indicate unchanged sections.
* IMPORTANT: For large files (e.g., > 300 lines), specify the range of lines to edit using `start` and `end` (1-indexed, inclusive). The range should be smaller than 300 lines.
* -1 indicates the last line of the file when used as the `start` or `end` value.
* Keep at least one unchanged line before the changed section and after the changed section wherever possible.
* Make sure to set the `start` and `end` to include all the lines in the original file referred to in the draft of the new file content. Failure to do so will result in bad edits.
* To append to a file, set both `start` and `end` to `-1`.
* If the file doesn't exist, a new file will be created with the provided content.
* IMPORTANT: Make sure you include all the required indentations for each line of code in the draft, otherwise the edited code will be incorrectly indented.
* IMPORTANT: Make sure that the first line of the draft is also properly indented and has the required whitespaces.
* IMPORTANT: NEVER include or make references to lines from outside the `start` and `end` range in the draft.
* IMPORTANT: Start the content with a comment in the format: #EDIT: Reason for edit
* IMPORTANT: If you are not appending to the file, avoid setting `start` and `end` to the same value.
**Example 1: general edit for short files**
For example, given an existing file `/path/to/file.py` that looks like this:
@@ -41,12 +33,13 @@ The assistant wants to edit the file to look like this:
The assistant may produce an edit action like this:
path="/path/to/file.txt" start=1 end=-1
content=```
#EDIT: I want to change the value of y to 2
class MyClass:
def __init__(self):
# ... existing code ...
# no changes before
self.y = 2
# self.z is removed
# MyClass().z is removed
print(MyClass().y)
```
@@ -65,7 +58,6 @@ For example, given an existing file `/path/to/file.py` that looks like this:
To append the following lines to the file:
```python
#EDIT: I want to print the value of y
print(MyClass().y)
```
@@ -101,9 +93,9 @@ The assistant wants to edit the file to look like this:
(2000 more lines below)
The assistant may produce an edit action like this:
path="/path/to/file.txt" start=1002 end=1008
path="/path/to/file.txt" start=1001 end=1008
content=```
#EDIT: I want to change the value of y to 2
class MyClass:
def __init__(self):
# no changes before
self.y = 2
@@ -208,7 +208,7 @@ Note:
# for visualwebarena, webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
# initialize and retrieve the first observation by issuing an noop OP
# For non-benchmark browsing, the browser env starts with a blank page, and the agent is expected to first navigate to desired websites
return BrowseInteractiveAction(browser_actions='noop(1000)', return_axtree=True)
return BrowseInteractiveAction(browser_actions='noop(1000)')
for event in state.view:
if isinstance(event, BrowseInteractiveAction):
+6 -10
View File
@@ -158,8 +158,8 @@ async def modify_llm_settings_basic(
provider_completer = FuzzyWordCompleter(provider_list)
session = PromptSession(key_bindings=kb_cancel())
# Set default provider - prefer 'anthropic' if available, otherwise use the first provider
provider = 'anthropic' if 'anthropic' in provider_list else provider_list[0]
# Set default provider - use the first available provider from the list
provider = provider_list[0] if provider_list else 'openai'
model = None
api_key = None
@@ -196,11 +196,9 @@ async def modify_llm_settings_basic(
# Make sure the provider exists in organized_models
if provider not in organized_models:
# If the provider doesn't exist, prefer 'anthropic' if available, otherwise use the first provider
# If the provider doesn't exist, use the first available provider
provider = (
'anthropic'
if 'anthropic' in organized_models
else next(iter(organized_models.keys()))
next(iter(organized_models.keys())) if organized_models else 'openai'
)
provider_models = organized_models[provider]['models']
@@ -215,10 +213,8 @@ async def modify_llm_settings_basic(
]
provider_models = VERIFIED_ANTHROPIC_MODELS + provider_models
# Set default model to the first model in the list (which will be a verified model if available)
default_model = (
provider_models[0] if provider_models else 'claude-sonnet-4-20250514'
)
# Set default model to the first model in the list
default_model = provider_models[0] if provider_models else 'gpt-4'
# Show the default model but allow changing it
print_formatted_text(
+95 -26
View File
@@ -12,7 +12,7 @@ from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.application import Application
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import HTML, FormattedText, StyleAndTextTuples
from prompt_toolkit.formatted_text import HTML, StyleAndTextTuples
from prompt_toolkit.input import create_input
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
@@ -132,51 +132,113 @@ def display_initialization_animation(text: str, is_loaded: asyncio.Event) -> Non
def display_banner(session_id: str) -> None:
print_formatted_text(
HTML(r"""<gold>
banner_text = r"""<gold>
___ _ _ _
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|_|
</gold>"""),
style=DEFAULT_STYLE,
</gold>"""
# Use TextArea with focusable=True to allow text selection
banner_container = Frame(
TextArea(
text=banner_text.replace('<gold>', '').replace('</gold>', ''),
read_only=True,
style=COLOR_GOLD,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GOLD}',
)
print_container(banner_container)
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text('')
print_formatted_text(HTML(f'<grey>Initialized conversation {session_id}</grey>'))
version_container = Frame(
TextArea(
text=f'OpenHands CLI v{__version__}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GREY}',
)
print_container(version_container)
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text('')
session_container = Frame(
TextArea(
text=f'Initialized conversation {session_id}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GREY}',
)
print_formatted_text('')
print_container(session_container)
print_formatted_text('')
def display_welcome_message(message: str = '') -> None:
print_formatted_text(
HTML("<gold>Let's start building!</gold>\n"), style=DEFAULT_STYLE
# Use TextArea with focusable=True to allow text selection
welcome_container = Frame(
TextArea(
text="Let's start building!",
read_only=True,
style=COLOR_GOLD,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GOLD}',
)
print_container(welcome_container)
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text('')
if message:
print_formatted_text(
HTML(f'{message} <grey>Type /help for help</grey>'),
style=DEFAULT_STYLE,
)
message_text = f'{message} Type /help for help'
else:
print_formatted_text(
HTML('What do you want to build? <grey>Type /help for help</grey>'),
style=DEFAULT_STYLE,
)
message_text = 'What do you want to build? Type /help for help'
message_container = Frame(
TextArea(
text=message_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GREY}',
)
print_container(message_container)
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text('')
def display_initial_user_prompt(prompt: str) -> None:
print_formatted_text(
FormattedText(
[
('', '\n'),
(COLOR_GOLD, '> '),
('', prompt),
]
)
# Use TextArea with focusable=True to allow text selection
prompt_container = Frame(
TextArea(
text=f'> {prompt}',
read_only=True,
style=COLOR_GOLD,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GOLD}',
)
print_formatted_text('')
print_container(prompt_container)
# Prompt output display functions
@@ -224,6 +286,7 @@ def display_error(error: str) -> None:
read_only=True,
style='ansired',
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Error',
style='ansired',
@@ -239,6 +302,7 @@ def display_command(event: CmdRunAction) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Command',
style='ansiblue',
@@ -267,6 +331,7 @@ def display_command_output(output: str) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Command Output',
style=f'fg:{COLOR_GREY}',
@@ -282,6 +347,7 @@ def display_file_edit(event: FileEditObservation) -> None:
read_only=True,
wrap_lines=True,
lexer=CustomDiffLexer(),
focusable=True, # Allow focusing to enable text selection
),
title='File Edit',
style=f'fg:{COLOR_GREY}',
@@ -298,6 +364,7 @@ def display_file_read(event: FileReadObservation) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='File Read',
style=f'fg:{COLOR_GREY}',
@@ -316,6 +383,7 @@ def initialize_streaming_output():
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
)
container = Frame(
streaming_output_text_area,
@@ -425,6 +493,7 @@ def display_usage_metrics(usage_metrics: UsageMetrics) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Usage Metrics',
style=f'fg:{COLOR_GREY}',

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