Compare commits

..

15 Commits

Author SHA1 Message Date
mamoodi 8f7c53687f Release 0.35.0 2025-04-28 15:41:25 -04:00
dependabot[bot] f2725eeb3d chore(deps): bump the version-all group across 1 directory with 8 updates (#8123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-04-28 17:18:21 +00:00
Engel Nyst 1b63633030 Simplify microagents (#8114)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-04-28 15:00:06 +00:00
Ryan H. Tran 107789b5a8 Add missing last ps1 match output into combined output (#8097) 2025-04-28 21:06:26 +08:00
Panduka Muditha 04bdea5faf feat: Add CLI support for /new, /status and /settings (#8017)
Co-authored-by: Bashwara Undupitiya <bashwarau@verdentra.com>
2025-04-28 08:52:33 -04:00
Engel Nyst 2bad4ea3d2 Litellm emergency fix (#8116) 2025-04-28 08:36:45 +02:00
Rohit Malhotra 1c4c477b3f [Refactor]: Abstract resolver into classes (#7999)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-27 18:52:52 -04:00
Rohit Malhotra 391ba1d988 [Fix]: Move suggest task prompts to BE (#8109)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-27 16:32:15 -04:00
Engel Nyst 70f469b0c1 [chore] run full pre-commit (#8104) 2025-04-27 08:43:26 +02:00
Graham Neubig 8a5c6d3bef Fix long setup scripts failing (#8101)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-26 12:27:54 -04:00
tofarr 998e04e51b Fix for issue where running unit tests resets local settings (#8100)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-26 07:24:49 -06:00
Boxuan Li da7041b5e9 Allow cmd execution after running a background command (#8093) 2025-04-26 03:04:05 +00:00
Robert Brennan e4b7b31f48 Add VS Code tab alongside workspace, terminal, and jupyter tabs (#8033)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-25 15:30:50 -07:00
chuckbutkus 587c53f115 Maybe fix for pre-commit errors (#8082)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-04-25 21:10:00 +00:00
mamoodi 4d76f31610 Update Microagents documentation (#7978) 2025-04-25 16:52:55 -04:00
159 changed files with 6437 additions and 4700 deletions
+1 -1
View File
@@ -118,7 +118,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.34-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.35-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -52,17 +52,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-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.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+1 -1
View File
@@ -11,7 +11,7 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.34-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.35-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+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.34-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.35-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:
+8 -8
View File
@@ -4,7 +4,7 @@ const swaggerUiDist = require('swagger-ui-dist');
/**
* This script manually sets up Swagger UI for the Docusaurus documentation.
*
*
* Why we need this approach:
* 1. Docusaurus doesn't have a built-in way to integrate Swagger UI
* 2. We need to copy the necessary files from swagger-ui-dist to our static directory
@@ -26,15 +26,15 @@ const files = fs.readdirSync(swaggerUiDistPath);
files.forEach(file => {
const sourcePath = path.join(swaggerUiDistPath, file);
const targetPath = path.join(targetDir, file);
// Skip directories and non-essential files
if (fs.statSync(sourcePath).isDirectory() ||
file === 'package.json' ||
if (fs.statSync(sourcePath).isDirectory() ||
file === 'package.json' ||
file === 'README.md' ||
file.endsWith('.map')) {
return;
}
fs.copyFileSync(sourcePath, targetPath);
});
@@ -54,13 +54,13 @@ const indexHtml = `
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
@@ -99,4 +99,4 @@ const indexHtml = `
fs.writeFileSync(path.join(targetDir, 'index.html'), indexHtml);
console.log('Swagger UI files generated successfully in static/swagger-ui/');
console.log('Swagger UI files generated successfully in static/swagger-ui/');
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -61,7 +61,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -56,6 +56,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```
@@ -13,16 +13,16 @@
La façon la plus simple d'exécuter OpenHands est avec Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -44,7 +44,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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +42,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
@@ -25,7 +25,7 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
```bash
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \
@@ -82,5 +82,5 @@ docker network create openhands-network
# 分離されたネットワークで OpenHands を実行
docker run # ... \
--network openhands-network \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "escreva um script bash que imprima oi"
```
@@ -58,17 +58,17 @@
A maneira mais fácil de executar o OpenHands é no Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-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.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
Você encontrará o OpenHands em execução em http://localhost:3000!
@@ -4,7 +4,7 @@
Microagentes públicos são diretrizes especializadas acionadas por palavras-chave para todos os usuários do OpenHands.
Eles são definidos em arquivos markdown no diretório
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
Microagentes públicos:
- Monitoram comandos recebidos em busca de suas palavras-chave de acionamento.
@@ -15,7 +15,7 @@ Microagentes públicos:
## Microagentes Públicos Atuais
Para mais informações sobre microagentes específicos, consulte seus arquivos de documentação individuais no
diretório [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/).
diretório [`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/).
### Agente GitHub
**Arquivo**: `github.md`
@@ -59,7 +59,7 @@ yes | npm install package-name
## Contribuindo com um Microagente Público
Você pode criar seus próprios microagentes públicos adicionando novos arquivos markdown ao
diretório [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/).
diretório [`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/).
### Melhores Práticas para Microagentes Públicos
@@ -81,7 +81,7 @@ Antes de criar um microagente público, considere:
#### 2. Crie o Arquivo
Crie um novo arquivo markdown em [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/)
Crie um novo arquivo markdown em [`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/)
com um nome descritivo (por exemplo, `docker.md` para um agente focado em Docker).
Atualize o arquivo com o frontmatter necessário [de acordo com o formato exigido](./microagents-overview#microagent-format)
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -57,6 +57,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```
@@ -11,16 +11,16 @@
在 Docker 中运行 OpenHands 是最简单的方式。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
@@ -11,7 +11,7 @@
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -1,18 +1,17 @@
# Repository Customization
You can customize how OpenHands works with your repository by creating a
You can customize how OpenHands interacts with your repository by creating a
`.openhands` directory at the root level.
## Microagents
You can use microagents to extend the OpenHands prompts with information
about your project and how you want OpenHands to work. See
[Repository Microagents](../prompting/microagents-repo) for more information.
Microagents allow you to extend OpenHands prompts with information specific to your project and define how OpenHands
should function. See [Microagents Overview](../prompting/microagents-overview) for more information.
## Setup Script
You can add `.openhands/setup.sh`, which will be run every time OpenHands begins
working with your repository. This is a good place to install dependencies, set
environment variables, etc.
You can add a `.openhands/setup.sh` file, which will run every time OpenHands begins working with your repository.
This is an ideal location for installing dependencies, setting environment variables, and performing other setup tasks.
For example:
```bash
+2 -2
View File
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
+2 -2
View File
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,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.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+3 -3
View File
@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-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.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
You'll find OpenHands running at http://localhost:3000!
@@ -0,0 +1,49 @@
# Keyword-Triggered Microagents
## Purpose
Keyword-triggered microagents provide OpenHands with specific instructions that are activated when certain keywords
appear in the prompt. This is useful for tailoring behavior based on particular tools, languages, or frameworks.
## Microagent File
Create a keyword-triggered microagent (example: `.openhands/microagents/trigger-keyword.md`) to include instructions
that activate only for prompts with specific keywords.
## Frontmatter Syntax
Frontmatter is required for keyword-triggered microagents. It must be placed at the top of the file,
above the guidelines.
Enclose the frontmatter in triple dashes (---) and include the following fields:
| Field | Description | Required | Default |
|------------|--------------------------------------------------|----------|------------------|
| `name` | A unique identifier for the microagent. | Yes | 'default' |
| `type` | Type of microagent. Must be set to `knowledge`. | Yes | 'repo' |
| `triggers` | A list of keywords that activate the microagent. | Yes | None |
| `agent` | The agent this microagent applies to. | No | 'CodeActAgent' |
## Example
```
---
name: magic_word
type: knowledge
triggers:
- yummyhappy
- happyyummy
agent: CodeActAgent
---
The user has said the magic word. Respond with "That was delicious!"
```
Keyword-triggered microagents:
- Monitor incoming prompts for specified trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow defined guidelines and restrictions.
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
@@ -1,31 +1,40 @@
# Microagents Overview
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge, repository-specific context
and task-specific workflows. They help by providing expert guidance, automating common tasks, and ensuring
consistent practices across projects.
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge.
They provide expert guidance, automate common tasks, and ensure consistent practices across projects.
## Microagent Categories
## Microagent Types
Currently OpenHands supports two categories of microagents:
Currently OpenHands supports the following types of microagents:
- [Repository-specific Microagents](./microagents-repo): Repository-specific context and guidelines for OpenHands.
- [Public Microagents](./microagents-public): General guidelines triggered by keywords for all OpenHands users.
- [General Repository Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
A microagent is classified as repository-specific or public depending on its location:
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
add `<microagent_name>.md` files inside.
- Repository-specific microagents are located in a repository's `.openhands/microagents/` directory
- Public microagents are located in the official OpenHands repository inside the `/microagents` folder
:::note
Loaded microagents take up space in the context window.
These microagents, alongside user messages, inform OpenHands about the task and the environment.
:::
When OpenHands works with a repository, it:
Example repository structure:
1. Loads **repository-specific** microagents from `.openhands/microagents/` if present in the repository.
2. Loads **public knowledge** microagents triggered by keywords in conversations
3. Loads **public tasks** microagents when explicitly requested by the user
```
some-repository/
└── .openhands/
└── microagents/
└── repo.md # General repository guidelines
└── trigger_this.md # Microagent triggered by specific keywords
└── trigger_that.md # Microagent triggered by specific keywords
```
You can check out the existing public microagents at the [official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/).
## Microagents Frontmatter Requirements
## Microagent Format
Each microagent file may include frontmatter that provides additional information. In some cases, this frontmatter
is required:
All microagents use markdown files with YAML frontmatter that have special instructions to help OpenHands activate them.
Check out the [syntax documentation](./microagents-syntax) for a comprehensive guide on how to configure your microagents.
| Microagent Type | Frontmatter Requirement |
|----------------------------------|-------------------------------------------------------|
| `General Repository Microagents` | Required only if more than one of this type exists. |
| `Keyword-Triggered Microagents` | Required. |
@@ -1,35 +1,16 @@
# Public Microagents
# Global Microagents
## Overview
Public microagents provide specialized context and capabilities for all OpenHands users, regardless of their repository configuration. Unlike repository-specific microagents, public microagents are globally available across all repositories.
Global microagents are [keyword-triggered microagents](./microagents-keyword) that apply to all OpenHands users.
Public microagents come in two types:
## Contributing a Global Microagent
- **Knowledge microagents**: Automatically activated when keywords in conversations match their triggers
- **Task microagents**: Explicitly invoked by users to guide through specific workflows
Both types follow the same syntax and structure as repository-specific microagents, using markdown files with YAML frontmatter that define their behavior and capabilities. They are located in the official OpenHands repository under:
- [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for knowledge microagents
- [`microagents/tasks/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks) for task microagents
Public microagents:
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
When loading public microagents, OpenHands scans the official repository's microagents directories recursively, processing all markdown files except README.md. The system categorizes each microagent based on its `type` field in the YAML frontmatter, regardless of its exact file location within the knowledge or tasks directories.
## Contributing a Public Microagent
You can create public microagents and share with the community by opening a pull request to the official repository.
You can create global microagents and share with the community by opening a pull request to the official repository.
See the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) for specific instructions on how to contribute to OpenHands.
### Public Microagents Best Practices
### Global Microagents Best Practices
- **Clear Scope**: Keep the microagent focused on a specific domain or task.
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
@@ -37,11 +18,11 @@ See the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CO
- **Safety First**: Include necessary warnings and constraints.
- **Integration Awareness**: Consider how the microagent interacts with other components.
### Steps to Contribute a Public Microagent
### Steps to Contribute a Global Microagent
#### 1. Plan the Public Microagent
#### 1. Plan the Global Microagent
Before creating a public microagent, consider:
Before creating a global microagent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
@@ -51,23 +32,19 @@ Before creating a public microagent, consider:
#### 2. Create File
Create a new Markdown file with a descriptive name in the appropriate directory:
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
- [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for knowledge microagents
- [`microagents/tasks/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks) for task microagents
#### 3. Testing the Global Microagent
Ensure it follows the correct [syntax](./microagents-syntax.md) and [best practices](./microagents-syntax.md#markdown-content-best-practices).
#### 3. Testing the Public Microagent
- Test the agent with various prompts
- Verify trigger words activate the agent correctly
- Ensure instructions are clear and comprehensive
- Check for potential conflicts and overlaps with existing agents
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts and overlaps with existing agents.
#### 4. Submission Process
Submit a pull request with:
- The new microagent file
- Updated documentation if needed
- Description of the agent's purpose and capabilities
- The new microagent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
+25 -104
View File
@@ -1,117 +1,38 @@
# Repository-specific Microagents
# General Repository Microagents
## Overview
## Purpose
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context and guidelines.
General guidelines for OpenHands to work more effectively with the repository.
This section explains how to optimize OpenHands for your project.
## Microagent File
## Creating Repository Microagents
Create a general repository microagent (example: `.openhands/microagents/repo.md`) to include
project-specific instructions, team practices, coding standards, and architectural guidelines that are relevant for
**all** prompts in that repository.
You can customize OpenHands' behavior for your repository by creating a `.openhands/microagents/` directory in your repository's root.
## Frontmatter Syntax
You can enhance OpenHands' performance by adding custom microagents to your repository:
The frontmatter for this type of microagent is optional, unless you plan to include more than one general
repository microagent.
1. For overall repository-specific instructions, create a `.openhands/microagents/repo.md` file
2. For reusable domain knowledge triggered by keywords, add multiple `.md` files to `.openhands/microagents/knowledge/`
3. For common workflows and tasks, create multiple `.md` files to `.openhands/microagents/tasks/`
Frontmatter should be enclosed in triple dashes (---) and may include the following fields:
Check out the [best practices](./microagents-syntax.md#markdown-content-best-practices) for formatting the content of your custom microagent.
| Field | Description | Required | Default |
|-----------|-----------------------------------------|--------------------------------------------------------------------|----------------|
| `name` | A unique identifier for the microagent | Required only if using more than one general repository microagent | 'default' |
| `agent` | The agent this microagent applies to | No | 'CodeActAgent' |
Keep in mind that loaded microagents take up space in the context window. It's crucial to strike a balance between the additional context provided by microagents and the instructions provided in the user's inputs.
Note that you can use OpenHands to create new microagents. The public microagent [`add_agent`](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/knowledge/add_agent.md) is loaded to all OpenHands instance and can support you on this.
## Types of Microagents
OpenHands supports three primary types of microagents, each with specific purposes and features to enhance agent performance:
- [repository](#repository-microagents)
- [knowledge](#knowledge-microagents)
- [tasks](#tasks-microagents)
The standard directory structure within a repository is:
- One main `repo.md` file containing repository-specific instructions
- Additional `Knowledge` agents in `.openhands/microagents/knowledge/` directory
- Additional `Task` agents in `.openhands/microagents/tasks/` directory
When processing the `.openhands/microagents/` directory, OpenHands will recursively scan all subfolders and process any `.md` files (except `README.md`) it finds. The system determines the microagent type based on the `type` field in the YAML frontmatter, not by the file's location. However, for organizational clarity, it's recommended to follow the standard directory structure.
### Repository Microagents
The `Repository` microagent is loaded specifically from `.openhands/microagents/repo.md` and serves as the main
repository-specific instruction file. This single file is automatically loaded whenever OpenHands works with that repository
without requiring any keyword matching or explicit call from the user.
OpenHands does not support multiple `repo.md` files in different locations or multiple microagents with type `repo`.
If you need to organize different types of repository information, the recommended approach is to use a single `repo.md` file with well-structured sections rather than trying to create multiple microagents with the type `repo`.
The best practice is to include project-specific instructions, team practices, coding standards, and architectural guidelines that are relevant for **all** prompts in that repository.
Example structure:
## Example
```
your-repository/
└── .openhands/
└── microagents/
└── repo.md # Repository-specific instructions
---
name: repo
---
This project is a TODO application that allows users to track TODO items.
To set it up, you can run `npm run build`.
Always make sure the tests are passing before committing changes. You can run the tests by running `npm run test`.
```
[See the example in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md?plain=1)
### Knowledge Microagents
Knowledge microagents provide specialized domain expertise:
- Recommended to be located in `.openhands/microagents/knowledge/`
- Triggered by specific keywords in conversations
- Contain expertise on tools, languages, frameworks, and common practices
Use knowledge microagents to trigger additional context relevant to specific technologies, tools, or workflows. For example, mentioning "git" in your conversation will automatically trigger git-related expertise to help with Git operations.
Examples structure:
```
your-repository/
└── .openhands/
└── microagents/
└── knowledge/
└── git.md
└── docker.md
└── python.md
└── ...
└── repo.md
```
You can find several real examples of `Knowledge` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
### Tasks Microagents
Task microagents guide users through interactive workflows:
- Recommended to be located in `.openhands/microagents/tasks/`
- Provide step-by-step processes for common development tasks
- Accept inputs and adapt to different scenarios
- Ensure consistent outcomes for complex operations
Task microagents are a convenient way to store multi-step processes you perform regularly. For instance, you can create a `update_pr_description.md` microagent to automatically generate better pull request descriptions based on code changes.
Examples structure:
```
your-repository/
└── .openhands/
└── microagents/
└── tasks/
└── update_pr_description.md
└── address_pr_comments.md
└── get_test_to_pass.md
└── ...
└── knowledge/
└── ...
└── repo.md
```
You can find several real examples of `Tasks` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks)
[See more examples of general repository microagents here.](https://github.com/All-Hands-AI/OpenHands/tree/main/.openhands/microagents)
@@ -1,128 +0,0 @@
# Microagents Syntax
Microagents are defined using markdown files with YAML frontmatter that specify their behavior, triggers, and capabilities.
Find below a comprehensive description of the frontmatter syntax and other details about how to use each type of microagent available at OpenHands.
## Frontmatter Schema
Every microagent requires a YAML frontmatter section at the beginning of the file, enclosed by triple dashes (`---`). The fields are:
| Field | Description | Required | Used By |
| ---------- | -------------------------------------------------- | ------------------------ | ---------------- |
| `name` | Unique identifier for the microagent | Yes | All types |
| `type` | Type of microagent: `repo`, `knowledge`, or `task` | Yes | All types |
| `version` | Version number (Semantic versioning recommended) | Yes | All types |
| `agent` | The agent type (typically `CodeActAgent`) | Yes | All types |
| `author` | Creator of the microagent | No | All types |
| `triggers` | List of keywords that activate the microagent | Yes for knowledge agents | Knowledge agents |
| `inputs` | Defines required user inputs for task execution | Yes for task agents | Task agents |
## Core Fields
### `agent`
**Purpose**: Specifies which agent implementation processes the microagent (typically `CodeActAgent`).
- Defines a single agent responsible for processing the microagent
- Must be available in the OpenHands system (see the [agent hub](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub))
- If the specified agent is not active, the microagent will not be used
### `triggers`
**Purpose**: Defines keywords that activate the `knowledge` microagent.
**Example**:
```yaml
triggers:
- kubernetes
- k8s
- docker
- security
- containers cluster
```
**Key points**:
- Can include both single words and multi-word phrases
- Case-insensitive matching is typically used
- More specific triggers (like "docker compose") prevent false activations
- Multiple triggers increase the chance of activation in relevant contexts
- Unique triggers like "flarglebargle" can be used for testing or special functionality
- Triggers should be carefully chosen to avoid unwanted activations or conflicts with other microagents
- Common terms used in many conversations may cause the microagent to be activated too frequently
When using multiple triggers, the microagent will be activated if any of the trigger words or phrases appear in the
conversation.
### `inputs`
**Purpose**: Defines parameters required from the user when a `task` microagent is activated.
**Schema**:
```yaml
inputs:
- name: INPUT_NAME # Used with {{ INPUT_NAME }}
description: 'Description of what this input is for'
required: true # Optional, defaults to true
```
**Key points**:
- The `name` and `description` properties are required for each input
- The `required` property is optional and defaults to `true`
- Input values are referenced in the microagent body using double curly braces (e.g., `{{ INPUT_NAME }}`)
- All inputs defined will be collected from the user before the task microagent executes
**Variable Usage**: Reference input values using double curly braces `{{ INPUT_NAME }}`.
## Example Formats
### Repository Microagent
Repository microagents provide context and guidelines for a specific repository.
- Located at: `.openhands/microagents/repo.md`
- Automatically loaded when working with the repository
- Only one per repository
The `Repository` microagent is loaded specifically from `.openhands/microagents/repo.md` and serves as the main
repository-specific instruction file. This single file is automatically loaded whenever OpenHands works with that repository
without requiring any keyword matching or explicit call from the user.
[See the example in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md?plain=1)
### Knowledge Microagent
Provides specialized domain expertise triggered by keywords.
You can find several real examples of `Knowledge` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
### Task Microagent
When explicitly asked by the user, will guide through interactive workflows with specific inputs.
You can find several real examples of `Tasks` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks)
## Markdown Content Best Practices
After the frontmatter, compose the microagent body using Markdown syntax. Examples of elements you can include are:
- Clear, concise instructions outlining the microagent's purpose and responsibilities
- Specific guidelines and constraints the microagent should adhere to
- Relevant code snippets and practical examples to illustrate key points
- Step-by-step procedures for task agents, guiding users through workflows
**Design Tips**:
- Keep microagents focused with a clear purpose
- Provide specific guidelines rather than general advice
- Use distinctive triggers for knowledge agents
- Keep content concise to minimize context window usage
- Break large microagents into smaller, focused ones
Aim for clarity, brevity, and practicality in your writing. Use formatting like bullet points, code blocks, and emphasis to enhance readability and comprehension.
Remember that balancing microagents details with user input space is important for maintaining effective interactions.
+6 -6
View File
@@ -66,18 +66,18 @@ const sidebars: SidebarsConfig = {
},
{
type: 'doc',
label: 'Repository-specific',
label: 'General Repository Microagents',
id: 'usage/prompting/microagents-repo',
},
{
type: 'doc',
label: 'Public',
id: 'usage/prompting/microagents-public',
label: 'Keyword-Triggered Microagents',
id: 'usage/prompting/microagents-keyword',
},
{
type: 'doc',
label: 'Syntax',
id: 'usage/prompting/microagents-syntax',
label: 'Global Microagents',
id: 'usage/prompting/microagents-public',
},
],
},
@@ -268,4 +268,4 @@ const sidebars: SidebarsConfig = {
],
};
export default sidebars;
export default sidebars;
+1 -1
View File
@@ -12,4 +12,4 @@ This file is used by the Swagger UI interface, which is accessible at `/swagger-
The OpenAPI specification is placed in the static directory so that it's accessible at a predictable URL in the deployed site. This allows the Swagger UI to reference it directly.
We only need one copy of the OpenAPI spec file, which is this one in the static directory.
We only need one copy of the OpenAPI spec file, which is this one in the static directory.
@@ -46,10 +46,12 @@ describe("HomeHeader", () => {
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
"gui",
undefined,
undefined,
[],
undefined,
undefined,
);
// expect to be redirected to /conversations/:conversationId
@@ -155,7 +155,7 @@ describe("RepoConnector", () => {
// select a repository from the dropdown
const dropdown = await waitFor(() =>
within(repoConnector).getByTestId("repo-dropdown")
within(repoConnector).getByTestId("repo-dropdown"),
);
await userEvent.click(dropdown);
@@ -164,6 +164,7 @@ describe("RepoConnector", () => {
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
"gui",
{
full_name: "rbren/polaris",
git_provider: "github",
@@ -173,6 +174,7 @@ describe("RepoConnector", () => {
undefined,
[],
undefined,
undefined,
);
});
@@ -11,12 +11,6 @@ import { AuthProvider } from "#/context/auth-context";
import { TaskCard } from "#/components/features/home/tasks/task-card";
import * as GitService from "#/api/git";
import { GitRepository } from "#/types/git";
import {
getFailingChecksPrompt,
getMergeConflictPrompt,
getOpenIssuePrompt,
getUnresolvedCommentsPrompt,
} from "#/components/features/home/tasks/get-prompt-for-query";
const MOCK_TASK_1: SuggestedTask = {
issue_number: 123,
@@ -101,7 +95,7 @@ describe("TaskCard", () => {
expect(createConversationSpy).toHaveBeenCalled();
});
describe("creating conversation prompts", () => {
describe("creating suggested task conversation", () => {
beforeEach(() => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
@@ -113,7 +107,7 @@ describe("TaskCard", () => {
});
});
it("should call create conversation with the merge conflict prompt", async () => {
it("should call create conversation with suggest task trigger and selected suggested task", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
renderTaskCard(MOCK_TASK_1);
@@ -122,74 +116,12 @@ describe("TaskCard", () => {
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledWith(
"suggested_task",
MOCK_RESPOSITORIES[0],
getMergeConflictPrompt(
MOCK_TASK_1.git_provider,
MOCK_TASK_1.issue_number,
MOCK_TASK_1.repo,
),
[],
undefined,
);
});
it("should call create conversation with the failing checks prompt", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
renderTaskCard(MOCK_TASK_2);
const launchButton = screen.getByTestId("task-launch-button");
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledWith(
MOCK_RESPOSITORIES[1],
getFailingChecksPrompt(
MOCK_TASK_2.git_provider,
MOCK_TASK_2.issue_number,
MOCK_TASK_2.repo,
),
[],
undefined,
);
});
it("should call create conversation with the unresolved comments prompt", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
renderTaskCard(MOCK_TASK_3);
const launchButton = screen.getByTestId("task-launch-button");
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledWith(
MOCK_RESPOSITORIES[2],
getUnresolvedCommentsPrompt(
MOCK_TASK_3.git_provider,
MOCK_TASK_3.issue_number,
MOCK_TASK_3.repo,
),
[],
undefined,
);
});
it("should call create conversation with the open issue prompt", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
renderTaskCard(MOCK_TASK_4);
const launchButton = screen.getByTestId("task-launch-button");
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledWith(
MOCK_RESPOSITORIES[3],
getOpenIssuePrompt(
MOCK_TASK_4.git_provider,
MOCK_TASK_4.issue_number,
MOCK_TASK_4.repo,
),
undefined,
[],
undefined,
MOCK_TASK_1,
);
});
});
@@ -1,28 +0,0 @@
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { describe, afterEach, vi, it, expect } from "vitest";
import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree";
const FILES = ["file-1-1.ts", "folder-1-2"];
describe.skip("ExplorerTree", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should render the explorer", () => {
renderWithProviders(<ExplorerTree files={FILES} defaultOpen />);
expect(screen.getByText("file-1-1.ts")).toBeInTheDocument();
expect(screen.getByText("folder-1-2")).toBeInTheDocument();
// TODO: make sure children render
});
it("should render the explorer given the defaultExpanded prop", () => {
renderWithProviders(<ExplorerTree files={FILES} />);
expect(screen.queryByText("file-1-1.ts")).toBeInTheDocument();
expect(screen.queryByText("folder-1-2")).toBeInTheDocument();
// TODO: make sure children don't render
});
});
@@ -1,64 +0,0 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { describe, it, expect, vi, afterEach } from "vitest";
import { AgentState } from "#/types/agent-state";
import { FileExplorer } from "#/components/features/file-explorer/file-explorer";
import { FileService } from "#/api/file-service/file-service.api";
const getFilesSpy = vi.spyOn(FileService, "getFiles");
vi.mock("../../services/fileService", async () => ({
uploadFiles: vi.fn(),
}));
const renderFileExplorerWithRunningAgentState = () =>
renderWithProviders(<FileExplorer isOpen onToggle={() => {}} />, {
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
},
},
});
describe.skip("FileExplorer", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("should get the workspace directory", async () => {
renderFileExplorerWithRunningAgentState();
expect(await screen.findByText("folder1")).toBeInTheDocument();
expect(await screen.findByText("file1.ts")).toBeInTheDocument();
expect(getFilesSpy).toHaveBeenCalledTimes(1); // once for root
});
it("should refetch the workspace when clicking the refresh button", async () => {
const user = userEvent.setup();
renderFileExplorerWithRunningAgentState();
expect(await screen.findByText("folder1")).toBeInTheDocument();
expect(await screen.findByText("file1.ts")).toBeInTheDocument();
expect(getFilesSpy).toHaveBeenCalledTimes(1); // once for root
const refreshButton = screen.getByTestId("refresh");
await user.click(refreshButton);
expect(getFilesSpy).toHaveBeenCalledTimes(2); // once for root, once for refresh button
});
it("should toggle the explorer visibility when clicking the toggle button", async () => {
const user = userEvent.setup();
renderFileExplorerWithRunningAgentState();
const folder1 = await screen.findByText("folder1");
expect(folder1).toBeInTheDocument();
const toggleButton = screen.getByTestId("toggle");
await user.click(toggleButton);
expect(folder1).toBeInTheDocument();
expect(folder1).not.toBeVisible();
});
});
@@ -1,110 +0,0 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { vi, describe, afterEach, it, expect } from "vitest";
import TreeNode from "#/components/features/file-explorer/tree-node";
import { FileService } from "#/api/file-service/file-service.api";
const getFileSpy = vi.spyOn(FileService, "getFile");
const getFilesSpy = vi.spyOn(FileService, "getFiles");
vi.mock("../../services/fileService", async () => ({
uploadFile: vi.fn(),
}));
describe.skip("TreeNode", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("should render a file if property has no children", () => {
renderWithProviders(<TreeNode path="/file.ts" defaultOpen />);
expect(screen.getByText("file.ts")).toBeInTheDocument();
});
it("should render a folder if it's in a subdir", async () => {
renderWithProviders(<TreeNode path="/folder1/" defaultOpen />);
expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
expect(await screen.findByText("folder1")).toBeInTheDocument();
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
});
it("should close a folder when clicking on it", async () => {
const user = userEvent.setup();
renderWithProviders(<TreeNode path="/folder1/" defaultOpen />);
const folder1 = await screen.findByText("folder1");
const file2 = await screen.findByText("file2.ts");
expect(folder1).toBeInTheDocument();
expect(file2).toBeInTheDocument();
await user.click(folder1);
expect(folder1).toBeInTheDocument();
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
});
it("should open a folder when clicking on it", async () => {
const user = userEvent.setup();
renderWithProviders(<TreeNode path="/folder1/" />);
const folder1 = await screen.findByText("folder1");
expect(folder1).toBeInTheDocument();
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
await user.click(folder1);
expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
expect(folder1).toBeInTheDocument();
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
});
it("should call `OpenHands.getFile` and return the full path of a file when clicking on a file", async () => {
const user = userEvent.setup();
renderWithProviders(<TreeNode path="/folder1/file2.ts" defaultOpen />);
const file2 = screen.getByText("file2.ts");
await user.click(file2);
expect(getFileSpy).toHaveBeenCalledWith("/folder1/file2.ts");
});
it("should render the full explorer given the defaultOpen prop", async () => {
const user = userEvent.setup();
renderWithProviders(<TreeNode path="/" defaultOpen />);
expect(getFilesSpy).toHaveBeenCalledWith("/");
const file1 = await screen.findByText("file1.ts");
const folder1 = await screen.findByText("folder1");
expect(file1).toBeInTheDocument();
expect(folder1).toBeInTheDocument();
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
await user.click(folder1);
expect(getFilesSpy).toHaveBeenCalledWith("folder1/");
expect(file1).toBeInTheDocument();
expect(folder1).toBeInTheDocument();
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
});
it("should render all children as collapsed when defaultOpen is false", async () => {
renderWithProviders(<TreeNode path="/folder1/" defaultOpen={false} />);
const folder1 = await screen.findByText("folder1");
expect(folder1).toBeInTheDocument();
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
await userEvent.click(folder1);
expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
expect(folder1).toBeInTheDocument();
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
});
});
+129 -129
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.34.0",
"version": "0.35.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.34.0",
"version": "0.35.0",
"dependencies": {
"@heroui/react": "2.7.6",
"@microlink/react-json-view": "^1.26.1",
@@ -17,22 +17,22 @@
"@reduxjs/toolkit": "^2.7.0",
"@stripe/react-stripe-js": "^3.6.0",
"@stripe/stripe-js": "^7.2.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query": "^5.74.7",
"@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.8.4",
"axios": "^1.9.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.9.1",
"framer-motion": "^12.9.2",
"i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.25",
"isbot": "^5.1.27",
"jose": "^6.0.10",
"lucide-react": "^0.503.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.236.6",
"posthog-js": "^1.236.7",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -60,12 +60,12 @@
"@playwright/test": "^1.52.0",
"@react-router/dev": "^7.5.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.73.3",
"@tanstack/eslint-plugin-query": "^5.74.7",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.14.1",
"@types/node": "^22.15.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.1",
"@types/react-highlight": "^0.12.8",
@@ -136,9 +136,9 @@
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.4.tgz",
"integrity": "sha512-SeuBV4rnjpFNjI8HSgKUwteuFdkHwkboq31HWzznuqgySQir+jSTczoWVVL4jvOjKjuH80fMDG0Fvg1Sb+OJsA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.5.tgz",
"integrity": "sha512-w7AmVyTTiU41fNLsFDf+gA2Dwtbx2EJtn2pbJNAGSRAg50loXy1uLXA3hEpD8+eydcomTurw09tq5/AyceCaGg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5977,9 +5977,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
"integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==",
"cpu": [
"arm"
],
@@ -5990,9 +5990,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz",
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz",
"integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==",
"cpu": [
"arm64"
],
@@ -6003,9 +6003,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz",
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz",
"integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==",
"cpu": [
"arm64"
],
@@ -6016,9 +6016,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz",
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz",
"integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==",
"cpu": [
"x64"
],
@@ -6029,9 +6029,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz",
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz",
"integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==",
"cpu": [
"arm64"
],
@@ -6042,9 +6042,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz",
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz",
"integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==",
"cpu": [
"x64"
],
@@ -6055,9 +6055,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz",
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz",
"integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==",
"cpu": [
"arm"
],
@@ -6068,9 +6068,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz",
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz",
"integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==",
"cpu": [
"arm"
],
@@ -6081,9 +6081,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz",
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz",
"integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==",
"cpu": [
"arm64"
],
@@ -6094,9 +6094,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz",
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz",
"integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==",
"cpu": [
"arm64"
],
@@ -6107,9 +6107,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz",
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz",
"integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==",
"cpu": [
"loong64"
],
@@ -6120,9 +6120,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz",
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz",
"integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==",
"cpu": [
"ppc64"
],
@@ -6133,9 +6133,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz",
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz",
"integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==",
"cpu": [
"riscv64"
],
@@ -6146,9 +6146,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz",
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz",
"integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==",
"cpu": [
"riscv64"
],
@@ -6159,9 +6159,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz",
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz",
"integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==",
"cpu": [
"s390x"
],
@@ -6172,9 +6172,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz",
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz",
"integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==",
"cpu": [
"x64"
],
@@ -6185,9 +6185,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz",
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
"integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
"cpu": [
"x64"
],
@@ -6198,9 +6198,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz",
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
"integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
"cpu": [
"arm64"
],
@@ -6211,9 +6211,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz",
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
"integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
"cpu": [
"ia32"
],
@@ -6224,9 +6224,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz",
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
"integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
"cpu": [
"x64"
],
@@ -6548,9 +6548,9 @@
}
},
"node_modules/@tanstack/eslint-plugin-query": {
"version": "5.73.3",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.73.3.tgz",
"integrity": "sha512-GmUtnOkRzDuNOq96g3eW5ADKC1nWfrM9RI0kRyQVr87rOl6y+PUgkuVaPxh3R2C0EVODxCS07b9aaWphidl/OA==",
"version": "5.74.7",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.74.7.tgz",
"integrity": "sha512-EeHuaaYiCOD+XOGyB7LMNEx9OEByAa5lkgP+S3ZggjKJpmIO6iRWeoIYYDKo2F8uc3qXcVhTfC7pn7NddQiNtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6565,9 +6565,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.74.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.4.tgz",
"integrity": "sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A==",
"version": "5.74.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.7.tgz",
"integrity": "sha512-X3StkN/Y6KGHndTjJf8H8th7AX4bKfbRpiVhVqevf0QWlxl6DhyJ0TYG3R0LARa/+xqDwzU9mA4pbJxzPCI29A==",
"license": "MIT",
"funding": {
"type": "github",
@@ -6575,12 +6575,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.74.4",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.4.tgz",
"integrity": "sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q==",
"version": "5.74.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.7.tgz",
"integrity": "sha512-u4o/RIWnnrq26orGZu2NDPwmVof1vtAiiV6KYUXd49GuK+8HX+gyxoAYqIaZogvCE1cqOuZAhQKcrKGYGkrLxg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.74.4"
"@tanstack/query-core": "5.74.7"
},
"funding": {
"type": "github",
@@ -6853,9 +6853,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"version": "22.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
"integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -7925,9 +7925,9 @@
}
},
"node_modules/axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -9165,9 +9165,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.142",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz",
"integrity": "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==",
"version": "1.5.143",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.143.tgz",
"integrity": "sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -10623,9 +10623,9 @@
}
},
"node_modules/framer-motion": {
"version": "12.9.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.9.1.tgz",
"integrity": "sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==",
"version": "12.9.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.9.2.tgz",
"integrity": "sha512-R0O3Jdqbfwywpm45obP+8sTgafmdEcUoShQTAV+rB5pi+Y1Px/FYL5qLLRe5tPtBdN1J4jos7M+xN2VV2oEAbQ==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.9.1",
@@ -10968,9 +10968,9 @@
"license": "MIT"
},
"node_modules/graphql": {
"version": "16.10.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz",
"integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==",
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -12031,9 +12031,9 @@
"license": "MIT"
},
"node_modules/isbot": {
"version": "5.1.26",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.26.tgz",
"integrity": "sha512-3wqJEYSIm59dYQjEF7zJ7T42aqaqxbCyJQda5rKCudJykuAnISptCHR/GSGpOnw8UrvU+mGueNLRJS5HXnbsXQ==",
"version": "5.1.27",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.27.tgz",
"integrity": "sha512-V3W56Hnztt4Wdh3VUlAMbdNicX/tOM38eChW3a2ixP6KEBJAeehxzYzTD59JrU5NCTgBZwRt9lRWr8D7eMZVYQ==",
"license": "Unlicense",
"engines": {
"node": ">=18"
@@ -14919,9 +14919,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.236.6",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.236.6.tgz",
"integrity": "sha512-IX4fkn3HCK+ObdHr/AuWd+Ks7bgMpRpOQB93b5rDJAWkG4if4xFVUn5pgEjyCNeOO2GM1ECnp08q9tYNYEfwbA==",
"version": "1.236.7",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.236.7.tgz",
"integrity": "sha512-HatTinqAt/6aAraCgbnP+2MTeVTChdf6TDsQkef4/yUnXeA4tsHmXnGGJ3vnzQk7N//R6lIHN189BZDO9kuKAg==",
"license": "MIT",
"dependencies": {
"core-js": "^3.38.1",
@@ -15922,9 +15922,9 @@
}
},
"node_modules/rollup": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz",
"integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
@@ -15937,26 +15937,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.40.0",
"@rollup/rollup-android-arm64": "4.40.0",
"@rollup/rollup-darwin-arm64": "4.40.0",
"@rollup/rollup-darwin-x64": "4.40.0",
"@rollup/rollup-freebsd-arm64": "4.40.0",
"@rollup/rollup-freebsd-x64": "4.40.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.40.0",
"@rollup/rollup-linux-arm-musleabihf": "4.40.0",
"@rollup/rollup-linux-arm64-gnu": "4.40.0",
"@rollup/rollup-linux-arm64-musl": "4.40.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.40.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.0",
"@rollup/rollup-linux-riscv64-gnu": "4.40.0",
"@rollup/rollup-linux-riscv64-musl": "4.40.0",
"@rollup/rollup-linux-s390x-gnu": "4.40.0",
"@rollup/rollup-linux-x64-gnu": "4.40.0",
"@rollup/rollup-linux-x64-musl": "4.40.0",
"@rollup/rollup-win32-arm64-msvc": "4.40.0",
"@rollup/rollup-win32-ia32-msvc": "4.40.0",
"@rollup/rollup-win32-x64-msvc": "4.40.0",
"@rollup/rollup-android-arm-eabi": "4.40.1",
"@rollup/rollup-android-arm64": "4.40.1",
"@rollup/rollup-darwin-arm64": "4.40.1",
"@rollup/rollup-darwin-x64": "4.40.1",
"@rollup/rollup-freebsd-arm64": "4.40.1",
"@rollup/rollup-freebsd-x64": "4.40.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.40.1",
"@rollup/rollup-linux-arm-musleabihf": "4.40.1",
"@rollup/rollup-linux-arm64-gnu": "4.40.1",
"@rollup/rollup-linux-arm64-musl": "4.40.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.40.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.1",
"@rollup/rollup-linux-riscv64-gnu": "4.40.1",
"@rollup/rollup-linux-riscv64-musl": "4.40.1",
"@rollup/rollup-linux-s390x-gnu": "4.40.1",
"@rollup/rollup-linux-x64-gnu": "4.40.1",
"@rollup/rollup-linux-x64-musl": "4.40.1",
"@rollup/rollup-win32-arm64-msvc": "4.40.1",
"@rollup/rollup-win32-ia32-msvc": "4.40.1",
"@rollup/rollup-win32-x64-msvc": "4.40.1",
"fsevents": "~2.3.2"
}
},
@@ -17507,9 +17507,9 @@
}
},
"node_modules/type-fest": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz",
"integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz",
"integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.34.0",
"version": "0.35.0",
"private": true,
"type": "module",
"engines": {
@@ -16,22 +16,22 @@
"@reduxjs/toolkit": "^2.7.0",
"@stripe/react-stripe-js": "^3.6.0",
"@stripe/stripe-js": "^7.2.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query": "^5.74.7",
"@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.8.4",
"axios": "^1.9.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.9.1",
"framer-motion": "^12.9.2",
"i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.25",
"isbot": "^5.1.27",
"jose": "^6.0.10",
"lucide-react": "^0.503.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.236.6",
"posthog-js": "^1.236.7",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -84,12 +84,12 @@
"@playwright/test": "^1.52.0",
"@react-router/dev": "^7.5.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.73.3",
"@tanstack/eslint-plugin-query": "^5.74.7",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.14.1",
"@types/node": "^22.15.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.1",
"@types/react-highlight": "^0.12.8",
@@ -39,6 +39,7 @@ const IGNORE_PATHS = [
"entry.client.tsx", // Client entry point
"utils/scan-unlocalized-strings.ts", // Original scanner
"utils/scan-unlocalized-strings-ast.ts", // This file itself
"frontend/src/components/features/home/tasks/get-prompt-for-query.ts", // Only contains agent prompts
];
// Extensions to scan
+6
View File
@@ -10,10 +10,12 @@ import {
GetTrajectoryResponse,
GitChangeDiff,
GitChange,
ConversationTrigger,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings, PostApiSettings } from "#/types/settings";
import { GitUser, GitRepository } from "#/types/git";
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
class OpenHands {
/**
@@ -149,17 +151,21 @@ class OpenHands {
}
static async createConversation(
conversation_trigger: ConversationTrigger = "gui",
selectedRepository?: GitRepository,
initialUserMsg?: string,
imageUrls?: string[],
replayJson?: string,
suggested_task?: SuggestedTask,
): Promise<Conversation> {
const body = {
conversation_trigger,
selected_repository: selectedRepository,
selected_branch: undefined,
initial_user_msg: initialUserMsg,
image_urls: imageUrls,
replay_json: replayJson,
suggested_task,
};
const { data } = await openHands.post<Conversation>(
+1 -1
View File
@@ -70,7 +70,7 @@ export interface AuthenticateResponse {
error?: string;
}
export type ConversationTrigger = "resolver" | "gui";
export type ConversationTrigger = "resolver" | "gui" | "suggested_task";
export interface Conversation {
conversation_id: string;
@@ -1,28 +0,0 @@
import { useTranslation } from "react-i18next";
import TreeNode from "./tree-node";
import { I18nKey } from "#/i18n/declaration";
interface ExplorerTreeProps {
files: string[] | null;
defaultOpen?: boolean;
}
export function ExplorerTree({
files,
defaultOpen = false,
}: ExplorerTreeProps) {
const { t } = useTranslation();
if (!files?.length) {
const message = !files
? I18nKey.EXPLORER$LOADING_WORKSPACE_MESSAGE
: I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE;
return <div className="text-sm text-gray-400 pt-4">{t(message)}</div>;
}
return (
<div className="w-full h-full pt-[4px]">
{files.map((file) => (
<TreeNode key={file} path={file} defaultOpen={defaultOpen} />
))}
</div>
);
}
@@ -1,28 +0,0 @@
import { RefreshIconButton } from "#/components/shared/buttons/refresh-icon-button";
import { ToggleWorkspaceIconButton } from "#/components/shared/buttons/toggle-workspace-icon-button";
import { cn } from "#/utils/utils";
interface ExplorerActionsProps {
onRefresh: () => void;
toggleHidden: () => void;
isHidden: boolean;
}
export function ExplorerActions({
toggleHidden,
onRefresh,
isHidden,
}: ExplorerActionsProps) {
return (
<div
className={cn(
"flex h-[24px] items-center gap-1",
isHidden ? "right-3" : "right-2",
)}
>
{!isHidden && <RefreshIconButton onClick={onRefresh} />}
<ToggleWorkspaceIconButton isHidden={isHidden} onClick={toggleHidden} />
</div>
);
}
@@ -1,39 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import { ExplorerActions } from "./file-explorer-actions";
interface FileExplorerHeaderProps {
isOpen: boolean;
onToggle: () => void;
onRefreshWorkspace: () => void;
}
export function FileExplorerHeader({
isOpen,
onToggle,
onRefreshWorkspace,
}: FileExplorerHeaderProps) {
const { t } = useTranslation();
return (
<div
className={cn(
"sticky top-0 bg-base-secondary",
"flex items-center",
!isOpen ? "justify-center" : "justify-between",
)}
>
{isOpen && (
<div className="text-neutral-300 font-bold text-sm">
{t(I18nKey.EXPLORER$LABEL_WORKSPACE)}
</div>
)}
<ExplorerActions
isHidden={!isOpen}
toggleHidden={onToggle}
onRefresh={onRefreshWorkspace}
/>
</div>
);
}
@@ -1,97 +0,0 @@
import React from "react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree";
import toast from "#/utils/toast";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import { useListFiles } from "#/hooks/query/use-list-files";
import { cn } from "#/utils/utils";
import { FileExplorerHeader } from "./file-explorer-header";
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
import { BrandButton } from "../settings/brand-button";
import VSCodeIcon from "#/assets/vscode-alt.svg?react";
interface FileExplorerProps {
isOpen: boolean;
onToggle: () => void;
}
export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
const { t } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { data: paths, refetch, error } = useListFiles();
const { data: vscodeUrl } = useVSCodeUrl({
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
});
const handleOpenVSCode = () => {
if (vscodeUrl?.vscode_url) {
window.open(vscodeUrl.vscode_url, "_blank");
} else if (vscodeUrl?.error) {
toast.error(
`open-vscode-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
error: vscodeUrl.error,
}),
);
}
};
const refreshWorkspace = () => {
if (!RUNTIME_INACTIVE_STATES.includes(curAgentState)) {
refetch();
}
};
React.useEffect(() => {
refreshWorkspace();
}, [curAgentState]);
return (
<div data-testid="file-explorer" className="relative h-full">
<div
className={cn(
"bg-base-secondary h-full border-r-1 border-r-neutral-600 flex flex-col",
!isOpen ? "w-12" : "w-60",
)}
>
<div className="flex flex-col relative h-full px-3 py-2 overflow-hidden">
<FileExplorerHeader
isOpen={isOpen}
onToggle={onToggle}
onRefreshWorkspace={refreshWorkspace}
/>
{!error && (
<div className="overflow-auto flex-grow min-h-0">
<div style={{ display: !isOpen ? "none" : "block" }}>
<ExplorerTree files={paths || []} />
</div>
</div>
)}
{error && (
<div className="flex flex-col items-center justify-center h-full">
<p className="text-neutral-300 text-sm">{error.message}</p>
</div>
)}
{isOpen && (
<BrandButton
testId="open-vscode-button"
type="button"
variant="secondary"
className="w-full text-content border-content"
isDisabled={RUNTIME_INACTIVE_STATES.includes(curAgentState)}
onClick={handleOpenVSCode}
startContent={<VSCodeIcon width={20} height={20} />}
>
{t(I18nKey.VSCODE$OPEN)}
</BrandButton>
)}
</div>
</div>
</div>
);
}
@@ -1,12 +0,0 @@
import { FaFile } from "react-icons/fa";
import { getExtension } from "#/utils/utils";
import { EXTENSION_ICON_MAP } from "../../extension-icon-map.constant";
interface FileIconProps {
filename: string;
}
export function FileIcon({ filename }: FileIconProps) {
const extension = getExtension(filename);
return EXTENSION_ICON_MAP[extension] || <FaFile />;
}
@@ -1,20 +0,0 @@
import { FolderIcon } from "./folder-icon";
import { FileIcon } from "./file-icon";
interface FilenameProps {
name: string;
type: "folder" | "file";
isOpen: boolean;
}
export function Filename({ name, type, isOpen }: FilenameProps) {
return (
<div className="cursor-pointer text-nowrap rounded-[5px] p-1 nowrap flex items-center gap-2 aria-selected:bg-neutral-600 aria-selected:text-white hover:text-white">
<div className="flex-shrink-0">
{type === "folder" && <FolderIcon isOpen={isOpen} />}
{type === "file" && <FileIcon filename={name} />}
</div>
<div className="flex-grow">{name}</div>
</div>
);
}
@@ -1,13 +0,0 @@
import { FaFolder, FaFolderOpen } from "react-icons/fa";
interface FolderIconProps {
isOpen: boolean;
}
export function FolderIcon({ isOpen }: FolderIconProps) {
return isOpen ? (
<FaFolderOpen color="D9D3D0" className="icon" />
) : (
<FaFolder color="D9D3D0" className="icon" />
);
}
@@ -1,88 +0,0 @@
import React from "react";
import { useSelector } from "react-redux";
import { useFiles } from "#/context/files";
import { cn } from "#/utils/utils";
import { useListFiles } from "#/hooks/query/use-list-files";
import { useListFile } from "#/hooks/query/use-list-file";
import { Filename } from "./filename";
import { RootState } from "#/store";
interface TreeNodeProps {
path: string;
defaultOpen?: boolean;
}
function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
const { setFileContent, setSelectedPath, files, selectedPath } = useFiles();
const [isOpen, setIsOpen] = React.useState(defaultOpen);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const isDirectory = path.endsWith("/");
const { data: paths } = useListFiles({
path,
enabled: isDirectory && isOpen,
});
const { data: fileContent, refetch } = useListFile({ path });
React.useEffect(() => {
if (fileContent) {
if (fileContent !== files[path]) {
setFileContent(path, fileContent);
}
}
}, [fileContent, path]);
React.useEffect(() => {
if (selectedPath === path && !isDirectory) {
refetch();
}
}, [curAgentState, selectedPath, path, isDirectory]);
const fileParts = path.split("/");
const filename =
fileParts[fileParts.length - 1] || fileParts[fileParts.length - 2];
const handleClick = async () => {
if (isDirectory) setIsOpen((prev) => !prev);
else {
setSelectedPath(path);
await refetch();
}
};
return (
<div
className={cn(
"text-sm text-neutral-400",
path === selectedPath && "bg-gray-700",
)}
>
<button
type={isDirectory ? "button" : "submit"}
name="file"
value={path}
onClick={handleClick}
className="flex items-center justify-between w-full px-1"
>
<Filename
name={filename}
type={isDirectory ? "folder" : "file"}
isOpen={isOpen}
/>
</button>
{isOpen && paths && (
<div className="ml-5">
{paths.map((child, index) => (
<TreeNode key={index} path={child} />
))}
</div>
)}
</div>
);
}
export default React.memo(TreeNode);
@@ -1,33 +0,0 @@
import React from "react";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { setInitialPrompt } from "#/state/initial-query-slice";
const INITIAL_PROMPT = "";
export function CodeNotInGitLink() {
const dispatch = useDispatch();
const { t } = useTranslation();
const { mutate: createConversation } = useCreateConversation();
const handleStartFromScratch = () => {
// Set the initial prompt and create a new conversation
dispatch(setInitialPrompt(INITIAL_PROMPT));
createConversation({ q: INITIAL_PROMPT });
};
return (
<div className="text-xs text-neutral-400">
{t(I18nKey.GITHUB$CODE_NOT_IN_GITHUB)}{" "}
<span
onClick={handleStartFromScratch}
className="underline cursor-pointer"
>
{t(I18nKey.GITHUB$START_FROM_SCRATCH)}
</span>{" "}
{t(I18nKey.GITHUB$VSCODE_LINK_DESCRIPTION)}
</div>
);
}
@@ -28,7 +28,7 @@ export function HomeHeader() {
testId="header-launch-button"
variant="primary"
type="button"
onClick={() => createConversation({})}
onClick={() => createConversation({ conversation_trigger: "gui" })}
isDisabled={isCreatingConversation}
>
{!isCreatingConversation && "Launch from Scratch"}
@@ -142,7 +142,12 @@ export function RepositorySelectionForm({
isLoadingRepositories ||
isRepositoriesError
}
onClick={() => createConversation({ selectedRepository })}
onClick={() =>
createConversation({
selectedRepository,
conversation_trigger: "gui",
})
}
>
{!isCreatingConversation && "Launch"}
{isCreatingConversation && t("HOME$LOADING")}
@@ -1,95 +0,0 @@
import { Provider } from "#/types/settings";
import { SuggestedTaskType } from "./task.types";
// Helper function to get provider-specific terminology
const getProviderTerms = (git_provider: Provider) => {
if (git_provider === "gitlab") {
return {
requestType: "Merge Request",
requestTypeShort: "MR",
apiName: "GitLab API",
tokenEnvVar: "GITLAB_TOKEN",
ciSystem: "CI pipelines",
ciProvider: "GitLab",
requestVerb: "merge request",
};
}
return {
requestType: "Pull Request",
requestTypeShort: "PR",
apiName: "GitHub API",
tokenEnvVar: "GITHUB_TOKEN",
ciSystem: "GitHub Actions",
ciProvider: "GitHub",
requestVerb: "pull request",
};
};
export const getMergeConflictPrompt = (
git_provider: Provider,
issueNumber: number,
repo: string,
) => {
const terms = getProviderTerms(git_provider);
return `You are working on ${terms.requestType} #${issueNumber} in repository ${repo}. You need to fix the merge conflicts.
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the ${terms.requestTypeShort} details. Check out the branch from that ${terms.requestVerb} and look at the diff versus the base branch of the ${terms.requestTypeShort} to understand the ${terms.requestTypeShort}'s intention.
Then resolve the merge conflicts. If you aren't sure what the right solution is, look back through the commit history at the commits that introduced the conflict and resolve them accordingly.`;
};
export const getFailingChecksPrompt = (
git_provider: Provider,
issueNumber: number,
repo: string,
) => {
const terms = getProviderTerms(git_provider);
return `You are working on ${terms.requestType} #${issueNumber} in repository ${repo}. You need to fix the failing CI checks.
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the ${terms.requestTypeShort} details. Check out the branch from that ${terms.requestVerb} and look at the diff versus the base branch of the ${terms.requestTypeShort} to understand the ${terms.requestTypeShort}'s intention.
Then use the ${terms.apiName} to look at the ${terms.ciSystem} that are failing on the most recent commit. Try and reproduce the failure locally.
Get things working locally, then push your changes. Sleep for 30 seconds at a time until the ${terms.ciProvider} ${terms.ciSystem.toLowerCase()} have run again. If they are still failing, repeat the process.`;
};
export const getUnresolvedCommentsPrompt = (
git_provider: Provider,
issueNumber: number,
repo: string,
) => {
const terms = getProviderTerms(git_provider);
return `You are working on ${terms.requestType} #${issueNumber} in repository ${repo}. You need to resolve the remaining comments from reviewers.
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the ${terms.requestTypeShort} details. Check out the branch from that ${terms.requestVerb} and look at the diff versus the base branch of the ${terms.requestTypeShort} to understand the ${terms.requestTypeShort}'s intention.
Then use the ${terms.apiName} to retrieve all the feedback on the ${terms.requestTypeShort} so far. If anything hasn't been addressed, address it and commit your changes back to the same branch.`;
};
export const getOpenIssuePrompt = (
git_provider: Provider,
issueNumber: number,
repo: string,
) => {
const terms = getProviderTerms(git_provider);
return `You are working on Issue #${issueNumber} in repository ${repo}. Your goal is to fix the issue.
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the issue details and any comments on the issue. Then check out a new branch and investigate what changes will need to be made.
Finally, make the required changes and open up a ${terms.requestVerb}. Be sure to reference the issue in the ${terms.requestTypeShort} description.`;
};
export const getPromptForQuery = (
git_provider: Provider,
type: SuggestedTaskType,
issueNumber: number,
repo: string,
) => {
switch (type) {
case "MERGE_CONFLICTS":
return getMergeConflictPrompt(git_provider, issueNumber, repo);
case "FAILING_CHECKS":
return getFailingChecksPrompt(git_provider, issueNumber, repo);
case "UNRESOLVED_COMMENTS":
return getUnresolvedCommentsPrompt(git_provider, issueNumber, repo);
case "OPEN_ISSUE":
return getOpenIssuePrompt(git_provider, issueNumber, repo);
default:
return "";
}
};
@@ -4,7 +4,6 @@ import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation"
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { cn } from "#/utils/utils";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { getPromptForQuery } from "./get-prompt-for-query";
import { TaskIssueNumber } from "./task-issue-number";
import { Provider } from "#/types/settings";
@@ -40,16 +39,11 @@ export function TaskCard({ task }: TaskCardProps) {
const handleLaunchConversation = () => {
const repo = getRepo(task.repo, task.git_provider);
const query = getPromptForQuery(
task.git_provider,
task.task_type,
task.issue_number,
task.repo,
);
return createConversation({
conversation_trigger: "suggested_task",
selectedRepository: repo,
q: query,
suggested_task: task,
});
};
@@ -18,7 +18,7 @@ function Terminal() {
return (
<div className="h-full p-2 min-h-0 flex-grow">
{isRuntimeInactive && (
<div className="text-sm text-gray-400 mb-2">
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
</div>
)}
+14 -10
View File
@@ -10,6 +10,7 @@ interface ContainerProps {
icon?: React.ReactNode;
isBeta?: boolean;
isLoading?: boolean;
rightContent?: React.ReactNode;
}[];
children: React.ReactNode;
className?: React.HTMLAttributes<HTMLDivElement>["className"];
@@ -30,16 +31,19 @@ export function Container({
>
{labels && (
<div className="flex text-xs h-[36px]">
{labels.map(({ label: l, to, icon, isBeta, isLoading }) => (
<NavTab
key={to}
to={to}
label={l}
icon={icon}
isBeta={isBeta}
isLoading={isLoading}
/>
))}
{labels.map(
({ label: l, to, icon, isBeta, isLoading, rightContent }) => (
<NavTab
key={to}
to={to}
label={l}
icon={icon}
isBeta={isBeta}
isLoading={isLoading}
rightContent={rightContent}
/>
),
)}
</div>
)}
{!labels && label && (
+15 -5
View File
@@ -9,9 +9,17 @@ interface NavTabProps {
icon: React.ReactNode;
isBeta?: boolean;
isLoading?: boolean;
rightContent?: React.ReactNode;
}
export function NavTab({ to, label, icon, isBeta, isLoading }: NavTabProps) {
export function NavTab({
to,
label,
icon,
isBeta,
isLoading,
rightContent,
}: NavTabProps) {
return (
<NavLink
end
@@ -24,15 +32,17 @@ export function NavTab({ to, label, icon, isBeta, isLoading }: NavTabProps) {
)}
>
{({ isActive }) => (
<>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<div className={cn(isActive && "text-logo")}>{icon}</div>
{label}
{isBeta && <BetaBadge />}
</div>
{isLoading && <LoadingSpinner size="small" />}
</>
<div className="flex items-center gap-2">
{rightContent}
{isLoading && <LoadingSpinner size="small" />}
</div>
</div>
)}
</NavLink>
);
@@ -19,7 +19,7 @@ export function RefreshIconButton({ onClick }: RefreshIconButtonProps) {
/>
}
testId="refresh"
ariaLabel={t(I18nKey.WORKSPACE$REFRESH)}
ariaLabel={t("BUTTON$REFRESH" as I18nKey)}
onClick={onClick}
/>
);
@@ -1,39 +0,0 @@
import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io";
import { useTranslation } from "react-i18next";
import { IconButton } from "./icon-button";
import { I18nKey } from "#/i18n/declaration";
interface ToggleWorkspaceIconButtonProps {
onClick: () => void;
isHidden: boolean;
}
export function ToggleWorkspaceIconButton({
onClick,
isHidden,
}: ToggleWorkspaceIconButtonProps) {
const { t } = useTranslation();
return (
<IconButton
icon={
isHidden ? (
<IoIosArrowForward
size={20}
className="text-neutral-400 hover:text-neutral-100 transition"
/>
) : (
<IoIosArrowBack
size={20}
className="text-neutral-400 hover:text-neutral-100 transition"
/>
)
}
testId="toggle"
ariaLabel={
isHidden ? t(I18nKey.WORKSPACE$OPEN) : t(I18nKey.WORKSPACE$CLOSE)
}
onClick={onClick}
/>
);
}
+1 -1
View File
@@ -52,7 +52,7 @@ export function TaskForm({ ref }: TaskFormProps) {
const formData = new FormData(event.currentTarget);
const q = formData.get("q")?.toString();
createConversation({ q });
createConversation({ q, conversation_trigger: "gui" });
};
return (
-69
View File
@@ -1,69 +0,0 @@
import React from "react";
interface FilesContextType {
/**
* List of file paths in the workspace
*/
paths: string[];
/**
* Set the list of file paths in the workspace
* @param paths The list of file paths in the workspace
* @returns void
*/
setPaths: (paths: string[]) => void;
/**
* A map of file paths to their contents
*/
files: Record<string, string>;
/**
* Set the content of a file
* @param path The path of the file
* @param content The content of the file
* @returns void
*/
setFileContent: (path: string, content: string) => void;
selectedPath: string | null;
setSelectedPath: (path: string | null) => void;
}
const FilesContext = React.createContext<FilesContextType | undefined>(
undefined,
);
interface FilesProviderProps {
children: React.ReactNode;
}
function FilesProvider({ children }: FilesProviderProps) {
const [paths, setPaths] = React.useState<string[]>([]);
const [files, setFiles] = React.useState<Record<string, string>>({});
const [selectedPath, setSelectedPath] = React.useState<string | null>(null);
const setFileContent = React.useCallback((path: string, content: string) => {
setFiles((prev) => ({ ...prev, [path]: content }));
}, []);
const value = React.useMemo(
() => ({
paths,
setPaths,
files,
setFileContent,
selectedPath,
setSelectedPath,
}),
[paths, setPaths, files, setFileContent, selectedPath, setSelectedPath],
);
return <FilesContext value={value}>{children}</FilesContext>;
}
function useFiles() {
const context = React.useContext(FilesContext);
if (context === undefined) {
throw new Error("useFiles must be used within a FilesProvider");
}
return context;
}
export { FilesProvider, useFiles };
@@ -6,6 +6,8 @@ import OpenHands from "#/api/open-hands";
import { setInitialPrompt } from "#/state/initial-query-slice";
import { RootState } from "#/store";
import { GitRepository } from "#/types/git";
import { ConversationTrigger } from "#/api/open-hands.types";
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
export const useCreateConversation = () => {
const navigate = useNavigate();
@@ -19,16 +21,20 @@ export const useCreateConversation = () => {
return useMutation({
mutationKey: ["create-conversation"],
mutationFn: async (variables: {
conversation_trigger: ConversationTrigger;
q?: string;
selectedRepository?: GitRepository | null;
suggested_task?: SuggestedTask;
}) => {
if (variables.q) dispatch(setInitialPrompt(variables.q));
return OpenHands.createConversation(
variables.conversation_trigger,
variables.selectedRepository || undefined,
variables.q,
files,
replayJson || undefined,
variables.suggested_task || undefined,
);
},
onSuccess: async ({ conversation_id: conversationId }, { q }) => {
-16
View File
@@ -1,16 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useConversation } from "#/context/conversation-context";
import { FileService } from "#/api/file-service/file-service.api";
interface UseListFileConfig {
path: string;
}
export const useListFile = (config: UseListFileConfig) => {
const { conversationId } = useConversation();
return useQuery({
queryKey: ["files", conversationId, config.path],
queryFn: () => FileService.getFile(conversationId, config.path),
enabled: false, // don't fetch by default, trigger manually via `refetch`
});
};
@@ -1,29 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useSelector } from "react-redux";
import { useConversation } from "#/context/conversation-context";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { FileService } from "#/api/file-service/file-service.api";
interface UseListFilesConfig {
path?: string;
enabled?: boolean;
}
const DEFAULT_CONFIG: UseListFilesConfig = {
enabled: true,
};
export const useListFiles = (config: UseListFilesConfig = DEFAULT_CONFIG) => {
const { conversationId } = useConversation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
return useQuery({
queryKey: ["files", conversationId, config?.path],
queryFn: () => FileService.getFiles(conversationId, config?.path),
enabled: runtimeIsActive && !!config?.enabled,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
};
+10 -15
View File
@@ -34,8 +34,11 @@ export enum I18nKey {
SETTINGS$TITLE = "SETTINGS$TITLE",
CONVERSATION$START_NEW = "CONVERSATION$START_NEW",
ACCOUNT_SETTINGS$TITLE = "ACCOUNT_SETTINGS$TITLE",
WORKSPACE$TITLE = "WORKSPACE$TITLE",
WORKSPACE$TERMINAL_TAB_LABEL = "WORKSPACE$TERMINAL_TAB_LABEL",
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
WORKSPACE$JUPYTER_TAB_LABEL = "WORKSPACE$JUPYTER_TAB_LABEL",
WORKSPACE$CODE_EDITOR_TAB_LABEL = "WORKSPACE$CODE_EDITOR_TAB_LABEL",
WORKSPACE$TITLE = "WORKSPACE$TITLE",
TERMINAL$WAITING_FOR_CLIENT = "TERMINAL$WAITING_FOR_CLIENT",
CODE_EDITOR$FILE_SAVED_SUCCESSFULLY = "CODE_EDITOR$FILE_SAVED_SUCCESSFULLY",
CODE_EDITOR$SAVING_LABEL = "CODE_EDITOR$SAVING_LABEL",
@@ -63,14 +66,12 @@ export enum I18nKey {
ERROR$GENERIC = "ERROR$GENERIC",
GITHUB$AUTH_SCOPE = "GITHUB$AUTH_SCOPE",
FILE_SERVICE$INVALID_FILE_PATH = "FILE_SERVICE$INVALID_FILE_PATH",
WORKSPACE$PLANNER_TAB_LABEL = "WORKSPACE$PLANNER_TAB_LABEL",
WORKSPACE$JUPYTER_TAB_LABEL = "WORKSPACE$JUPYTER_TAB_LABEL",
WORKSPACE$CODE_EDITOR_TAB_LABEL = "WORKSPACE$CODE_EDITOR_TAB_LABEL",
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
WORKSPACE$REFRESH = "WORKSPACE$REFRESH",
WORKSPACE$OPEN = "WORKSPACE$OPEN",
WORKSPACE$CLOSE = "WORKSPACE$CLOSE",
VSCODE$OPEN = "VSCODE$OPEN",
VSCODE$TITLE = "VSCODE$TITLE",
VSCODE$LOADING = "VSCODE$LOADING",
VSCODE$URL_NOT_AVAILABLE = "VSCODE$URL_NOT_AVAILABLE",
VSCODE$FETCH_ERROR = "VSCODE$FETCH_ERROR",
VSCODE$IFRAME_PERMISSIONS = "VSCODE$IFRAME_PERMISSIONS",
INCREASE_TEST_COVERAGE = "INCREASE_TEST_COVERAGE",
AUTO_MERGE_PRS = "AUTO_MERGE_PRS",
FIX_README = "FIX_README",
@@ -134,10 +135,6 @@ export enum I18nKey {
SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE = "SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE",
EXPLORER$UPLOAD_ERROR_MESSAGE = "EXPLORER$UPLOAD_ERROR_MESSAGE",
EXPLORER$LABEL_DROP_FILES = "EXPLORER$LABEL_DROP_FILES",
EXPLORER$LABEL_WORKSPACE = "EXPLORER$LABEL_WORKSPACE",
EXPLORER$EMPTY_WORKSPACE_MESSAGE = "EXPLORER$EMPTY_WORKSPACE_MESSAGE",
EXPLORER$LOADING_WORKSPACE_MESSAGE = "EXPLORER$LOADING_WORKSPACE_MESSAGE",
EXPLORER$REFRESH_ERROR_MESSAGE = "EXPLORER$REFRESH_ERROR_MESSAGE",
EXPLORER$UPLOAD_SUCCESS_MESSAGE = "EXPLORER$UPLOAD_SUCCESS_MESSAGE",
EXPLORER$NO_FILES_UPLOADED_MESSAGE = "EXPLORER$NO_FILES_UPLOADED_MESSAGE",
EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE = "EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE",
@@ -288,6 +285,7 @@ export enum I18nKey {
BUTTON$CREATE = "BUTTON$CREATE",
BUTTON$DELETE = "BUTTON$DELETE",
BUTTON$COPY_TO_CLIPBOARD = "BUTTON$COPY_TO_CLIPBOARD",
BUTTON$REFRESH = "BUTTON$REFRESH",
ERROR$REQUIRED_FIELD = "ERROR$REQUIRED_FIELD",
PLANNER$EMPTY_MESSAGE = "PLANNER$EMPTY_MESSAGE",
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
@@ -348,9 +346,6 @@ export enum I18nKey {
BROWSER$SCREENSHOT_ALT = "BROWSER$SCREENSHOT_ALT",
ERROR_TOAST$CLOSE_BUTTON_LABEL = "ERROR_TOAST$CLOSE_BUTTON_LABEL",
FILE_EXPLORER$UPLOAD = "FILE_EXPLORER$UPLOAD",
FILE_EXPLORER$REFRESH_WORKSPACE = "FILE_EXPLORER$REFRESH_WORKSPACE",
FILE_EXPLORER$OPEN_WORKSPACE = "FILE_EXPLORER$OPEN_WORKSPACE",
FILE_EXPLORER$CLOSE_WORKSPACE = "FILE_EXPLORER$CLOSE_WORKSPACE",
ACTION_MESSAGE$RUN = "ACTION_MESSAGE$RUN",
ACTION_MESSAGE$RUN_IPYTHON = "ACTION_MESSAGE$RUN_IPYTHON",
ACTION_MESSAGE$READ = "ACTION_MESSAGE$READ",
+165 -225
View File
@@ -511,21 +511,7 @@
"tr": "Hesap ayarları",
"de": "Kontoeinstellungen"
},
"WORKSPACE$TITLE": {
"en": "Workspace",
"zh-CN": "工作区",
"de": "Arbeitsbereich",
"ko-KR": "워크스페이스",
"no": "Arbeidsområde",
"zh-TW": "工作區",
"ar": "مساحة العمل",
"fr": "Espace de travail",
"it": "Area di lavoro",
"pt": "Espaço de trabalho",
"es": "Espacio de trabajo",
"ja": "ワークスペース",
"tr": "Çalışma Alanı"
},
"WORKSPACE$TERMINAL_TAB_LABEL": {
"en": "Terminal",
"zh-CN": "终端",
@@ -541,6 +527,66 @@
"tr": "Terminal",
"ja": "ターミナル"
},
"WORKSPACE$BROWSER_TAB_LABEL": {
"en": "Browser",
"zh-CN": "浏览器",
"de": "Browser",
"ko-KR": "브라우저",
"no": "Nettleser",
"zh-TW": "瀏覽器",
"it": "Browser",
"pt": "Navegador",
"es": "Navegador",
"ar": "المتصفح",
"fr": "Navigateur",
"tr": "Tarayıcı",
"ja": "ブラウザ"
},
"WORKSPACE$JUPYTER_TAB_LABEL": {
"en": "Jupyter",
"zh-CN": "Jupyter",
"de": "Jupyter",
"ko-KR": "Jupyter",
"no": "Jupyter",
"zh-TW": "Jupyter",
"it": "Jupyter",
"pt": "Jupyter",
"es": "Jupyter",
"ar": "Jupyter",
"fr": "Jupyter",
"tr": "Jupyter",
"ja": "Jupyter"
},
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
"en": "Code Editor",
"zh-CN": "代码编辑器",
"de": "Code-Editor",
"ko-KR": "코드 에디터",
"no": "Kodeeditor",
"zh-TW": "程式碼編輯器",
"it": "Editor di codice",
"pt": "Editor de código",
"es": "Editor de código",
"ar": "محرر الكود",
"fr": "Éditeur de code",
"tr": "Kod Düzenleyici",
"ja": "コードエディタ"
},
"WORKSPACE$TITLE": {
"en": "Workspace",
"zh-CN": "工作区",
"de": "Arbeitsbereich",
"ko-KR": "작업 공간",
"no": "Arbeidsområde",
"zh-TW": "工作區",
"it": "Area di lavoro",
"pt": "Espaço de trabalho",
"es": "Espacio de trabajo",
"ar": "مساحة العمل",
"fr": "Espace de travail",
"tr": "Çalışma Alanı",
"ja": "ワークスペース"
},
"TERMINAL$WAITING_FOR_CLIENT": {
"en": "Waiting for client to become ready...",
"ja": "クライアントの準備を待機中...",
@@ -946,111 +992,13 @@
"tr": "Geçersiz dosya yolu. Lütfen dosya adını kontrol edin ve tekrar deneyin.",
"ja": "ファイルパスが無効です。ファイル名を確認して、もう一度お試しください。"
},
"WORKSPACE$PLANNER_TAB_LABEL": {
"en": "Planner",
"zh-CN": "规划器",
"de": "Planer",
"ko-KR": "플래너",
"no": "Planlegger",
"zh-TW": "規劃工具",
"ar": "المخطط",
"fr": "Planificateur",
"it": "Pianificatore",
"pt": "Planejador",
"es": "Planificador",
"ja": "プランナー",
"tr": "Planlayıcı"
},
"WORKSPACE$JUPYTER_TAB_LABEL": {
"en": "Jupyter",
"zh-CN": "Jupyter",
"de": "Jupyter",
"ko-KR": "Jupyter",
"no": "Jupyter",
"zh-TW": "Jupyter",
"ar": "Jupyter",
"fr": "Jupyter",
"it": "Jupyter",
"pt": "Jupyter",
"es": "Jupyter",
"ja": "Jupyter",
"tr": "Jupyter"
},
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
"en": "Code Editor",
"zh-CN": "代码编辑器",
"de": "Code-Editor",
"ko-KR": "코드 편집기",
"no": "Kode editor",
"zh-TW": "程式碼編輯器",
"ar": "محرر الكود",
"fr": "Éditeur de code",
"it": "Editor di codice",
"pt": "Editor de código",
"es": "Editor de código",
"ja": "コードエディタ",
"tr": "Kod Editörü"
},
"WORKSPACE$BROWSER_TAB_LABEL": {
"en": "Browser",
"zh-CN": "浏览器",
"de": "Browser",
"ko-KR": "브라우저",
"no": "Nettleser",
"zh-TW": "瀏覽器",
"it": "Browser",
"pt": "Navegador",
"es": "Navegador",
"ar": "المتصفح",
"fr": "Navigateur",
"tr": "Tarayıcı",
"ja": "ブラウザ"
},
"WORKSPACE$REFRESH": {
"en": "Refresh",
"de": "Aktualisieren",
"zh-CN": "刷新",
"zh-TW": "重新整理",
"ko-KR": "새로고침",
"no": "Oppdater",
"it": "Aggiorna",
"pt": "Atualizar",
"es": "Actualizar",
"ar": "تحديث",
"fr": "Rafraîchir",
"tr": "Yenile",
"ja": "更新"
},
"WORKSPACE$OPEN": {
"en": "Open workspace",
"de": "Arbeitsbereich öffnen",
"zh-CN": "打开工作区",
"zh-TW": "開啟工作區",
"ko-KR": "작업 공간 열기",
"no": "Åpne arbeidsområde",
"it": "Apri area di lavoro",
"pt": "Abrir espaço de trabalho",
"es": "Abrir espacio de trabajo",
"ar": "فتح مساحة العمل",
"fr": "Ouvrir l'espace de travail",
"tr": "Çalışma alanını aç",
"ja": "ワークスペースを開く"
},
"WORKSPACE$CLOSE": {
"en": "Close workspace",
"de": "Arbeitsbereich schließen",
"zh-CN": "关闭工作区",
"zh-TW": "關閉工作區",
"ko-KR": "작업 공간 닫기",
"no": "Lukk arbeidsområde",
"it": "Chiudi area di lavoro",
"pt": "Fechar espaço de trabalho",
"es": "Cerrar espacio de trabajo",
"ar": "إغلاق مساحة العمل",
"fr": "Fermer l'espace de travail",
"tr": "Çalışma alanını kapat",
"ja": "ワークスペースを閉じる"
},
"VSCODE$OPEN": {
"en": "Open in VS Code",
"ja": "VS Codeで開く",
@@ -1066,6 +1014,81 @@
"fr": "Ouvrir dans VS Code",
"tr": "VS Code'da aç"
},
"VSCODE$TITLE": {
"en": "VS Code",
"ja": "VS Code",
"zh-CN": "VS Code",
"zh-TW": "VS Code",
"ko-KR": "VS Code",
"de": "VS Code",
"no": "VS Code",
"it": "VS Code",
"pt": "VS Code",
"es": "VS Code",
"ar": "VS Code",
"fr": "VS Code",
"tr": "VS Code"
},
"VSCODE$LOADING": {
"en": "Loading VS Code...",
"ja": "VS Codeを読み込み中...",
"zh-CN": "正在加载VS Code...",
"zh-TW": "正在加載VS Code...",
"ko-KR": "VS Code 로딩 중...",
"de": "VS Code wird geladen...",
"no": "Laster VS Code...",
"it": "Caricamento di VS Code...",
"pt": "Carregando VS Code...",
"es": "Cargando VS Code...",
"ar": "جاري تحميل VS Code...",
"fr": "Chargement de VS Code...",
"tr": "VS Code yükleniyor..."
},
"VSCODE$URL_NOT_AVAILABLE": {
"en": "VS Code URL not available",
"ja": "VS Code URLが利用できません",
"zh-CN": "VS Code URL不可用",
"zh-TW": "VS Code URL不可用",
"ko-KR": "VS Code URL을 사용할 수 없습니다",
"de": "VS Code URL nicht verfügbar",
"no": "VS Code URL ikke tilgjengelig",
"it": "URL di VS Code non disponibile",
"pt": "URL do VS Code não disponível",
"es": "URL de VS Code no disponible",
"ar": "رابط VS Code غير متوفر",
"fr": "URL VS Code non disponible",
"tr": "VS Code URL'si mevcut değil"
},
"VSCODE$FETCH_ERROR": {
"en": "Failed to fetch VS Code URL",
"ja": "VS Code URLの取得に失敗しました",
"zh-CN": "获取VS Code URL失败",
"zh-TW": "獲取VS Code URL失敗",
"ko-KR": "VS Code URL을 가져오지 못했습니다",
"de": "Fehler beim Abrufen der VS Code URL",
"no": "Kunne ikke hente VS Code URL",
"it": "Impossibile recuperare l'URL di VS Code",
"pt": "Falha ao buscar URL do VS Code",
"es": "Error al obtener la URL de VS Code",
"ar": "فشل في جلب رابط VS Code",
"fr": "Échec de la récupération de l'URL VS Code",
"tr": "VS Code URL'si alınamadı"
},
"VSCODE$IFRAME_PERMISSIONS": {
"en": "clipboard-read; clipboard-write",
"ja": "clipboard-read; clipboard-write",
"zh-CN": "clipboard-read; clipboard-write",
"zh-TW": "clipboard-read; clipboard-write",
"ko-KR": "clipboard-read; clipboard-write",
"de": "clipboard-read; clipboard-write",
"no": "clipboard-read; clipboard-write",
"it": "clipboard-read; clipboard-write",
"pt": "clipboard-read; clipboard-write",
"es": "clipboard-read; clipboard-write",
"ar": "clipboard-read; clipboard-write",
"fr": "clipboard-read; clipboard-write",
"tr": "clipboard-read; clipboard-write"
},
"INCREASE_TEST_COVERAGE": {
"en": "Increase test coverage",
"ja": "テストカバレッジを向上",
@@ -1990,66 +2013,10 @@
"tr": "Dosyaları buraya bırakın",
"ja": "ここにファイルをドロップ"
},
"EXPLORER$LABEL_WORKSPACE": {
"en": "Workspace",
"zh-CN": "工作区",
"de": "Arbeitsbereich",
"zh-TW": "工作區",
"es": "Espacio de trabajo",
"fr": "Espace de travail",
"it": "Area di lavoro",
"pt": "Espaço de trabalho",
"ko-KR": "워크스페이스",
"ar": "مساحة العمل",
"tr": "Çalışma alanı",
"no": "Arbeidsområde",
"ja": "ワークスペース"
},
"EXPLORER$EMPTY_WORKSPACE_MESSAGE": {
"en": "No files in workspace",
"zh-CN": "工作区为空",
"de": "Keine Dateien im Arbeitsbereich",
"zh-TW": "工作區為空",
"es": "No hay archivos en el espacio de trabajo",
"fr": "Aucun fichier dans l'espace de travail",
"it": "Nessun file nell'area di lavoro",
"pt": "Nenhum arquivo no espaço de trabalho",
"ko-KR": "워크스페이스가 비어 있습니다",
"ar": "لا توجد ملفات في مساحة العمل",
"tr": "Çalışma alanında dosya yok",
"no": "Ingen filer i arbeidsområdet",
"ja": "ワークスペースにファイルがありません"
},
"EXPLORER$LOADING_WORKSPACE_MESSAGE": {
"en": "Loading workspace...",
"zh-CN": "加载工作区中...",
"de": "Arbeitsbereich wird geladen...",
"zh-TW": "載入工作區中...",
"es": "Cargando espacio de trabajo...",
"fr": "Chargement de l'espace de travail...",
"it": "Caricamento dell'area di lavoro...",
"pt": "Carregando espaço de trabalho...",
"ko-KR": "워크스페이스 로딩 중...",
"ar": "جارٍ تحميل مساحة العمل...",
"tr": "Çalışma alanı yükleniyor...",
"no": "Laster arbeidsområde...",
"ja": "ワークスペースを読み込み中..."
},
"EXPLORER$REFRESH_ERROR_MESSAGE": {
"en": "Error refreshing workspace",
"zh-CN": "刷新时出错",
"de": "Fehler beim Aktualisieren des Arbeitsbereichs",
"zh-TW": "重新整理時發生錯誤",
"es": "Error al actualizar el espacio de trabajo",
"fr": "Erreur lors de l'actualisation de l'espace de travail",
"it": "Errore durante l'aggiornamento dell'area di lavoro",
"pt": "Erro ao atualizar o espaço de trabalho",
"ko-KR": "새로고침 중 오류 발생",
"ar": "خطأ في تحديث مساحة العمل",
"tr": "Çalışma alanı yenilenirken hata oluştu",
"no": "Feil ved oppdatering av arbeidsområde",
"ja": "ワークスペースの更新中にエラーが発生しました"
},
"EXPLORER$UPLOAD_SUCCESS_MESSAGE": {
"en": "Successfully uploaded {{count}} file(s)",
"zh-CN": "上传成功",
@@ -4058,6 +4025,21 @@
"BUTTON$COPY_TO_CLIPBOARD": {
"en": "Copy to Clipboard"
},
"BUTTON$REFRESH": {
"en": "Refresh",
"ja": "更新",
"zh-CN": "刷新",
"zh-TW": "重新整理",
"ko-KR": "새로고침",
"no": "Oppdater",
"it": "Aggiorna",
"pt": "Atualizar",
"es": "Actualizar",
"ar": "تحديث",
"fr": "Rafraîchir",
"tr": "Yenile",
"de": "Aktualisieren"
},
"ERROR$REQUIRED_FIELD": {
"en": "This field is required"
},
@@ -4935,51 +4917,9 @@
"es": "Subir archivo",
"tr": "Dosya yükle"
},
"FILE_EXPLORER$REFRESH_WORKSPACE": {
"en": "Refresh workspace",
"zh-CN": "刷新工作区",
"zh-TW": "重新整理工作區",
"ko-KR": "워크스페이스 새로고침",
"ja": "ワークスペースを更新",
"no": "Oppdater arbeidsområde",
"ar": "تحديث مساحة العمل",
"de": "Arbeitsbereich aktualisieren",
"fr": "Actualiser l'espace de travail",
"it": "Aggiorna area di lavoro",
"pt": "Atualizar área de trabalho",
"es": "Actualizar espacio de trabajo",
"tr": "Çalışma alanını yenile"
},
"FILE_EXPLORER$OPEN_WORKSPACE": {
"en": "Open workspace",
"zh-CN": "打开工作区",
"zh-TW": "開啟工作區",
"ko-KR": "워크스페이스 열기",
"ja": "ワークスペースを開く",
"no": "Åpne arbeidsområde",
"ar": "فتح مساحة العمل",
"de": "Arbeitsbereich öffnen",
"fr": "Ouvrir l'espace de travail",
"it": "Apri area di lavoro",
"pt": "Abrir área de trabalho",
"es": "Abrir espacio de trabajo",
"tr": "Çalışma alanını aç"
},
"FILE_EXPLORER$CLOSE_WORKSPACE": {
"en": "Close workspace",
"zh-CN": "关闭工作区",
"zh-TW": "關閉工作區",
"ko-KR": "워크스페이스 닫기",
"ja": "ワークスペースを閉じる",
"no": "Lukk arbeidsområde",
"ar": "إغلاق مساحة العمل",
"de": "Arbeitsbereich schließen",
"fr": "Fermer l'espace de travail",
"it": "Chiudi area di lavoro",
"pt": "Fechar área de trabalho",
"es": "Cerrar espacio de trabajo",
"tr": "Çalışma alanını kapat"
},
"ACTION_MESSAGE$RUN": {
"en": "Running <cmd>{{action.payload.args.command}}</cmd>",
"zh-CN": "运行 <cmd>{{action.payload.args.command}}</cmd>",
+1 -1
View File
@@ -15,11 +15,11 @@ export default [
]),
route("conversations/:conversationId", "routes/conversation.tsx", [
index("routes/editor.tsx"),
route("workspace", "routes/editor-tab.tsx"),
route("browser", "routes/browser-tab.tsx"),
route("jupyter", "routes/jupyter-tab.tsx"),
route("served", "routes/served-tab.tsx"),
route("terminal", "routes/terminal-tab.tsx"),
route("vscode", "routes/vscode-tab.tsx"),
]),
]),
] satisfies RouteConfig;
+38 -9
View File
@@ -2,10 +2,12 @@ import { useDisclosure } from "@heroui/react";
import React from "react";
import { Outlet } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { FaServer } from "react-icons/fa";
import { FaServer, FaExternalLinkAlt } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { DiGit } from "react-icons/di";
import { VscCode } from "react-icons/vsc";
import { I18nKey } from "#/i18n/declaration";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import {
ConversationProvider,
useConversation,
@@ -14,12 +16,12 @@ import { Controls } from "#/components/features/controls/controls";
import { clearMessages, addUserMessage } from "#/state/chat-slice";
import { clearTerminal } from "#/state/command-slice";
import { useEffectOnce } from "#/hooks/use-effect-once";
import CodeIcon from "#/icons/code.svg?react";
import GlobeIcon from "#/icons/globe.svg?react";
import JupyterIcon from "#/icons/jupyter.svg?react";
import TerminalIcon from "#/icons/terminal.svg?react";
import { clearJupyter } from "#/state/jupyter-slice";
import { FilesProvider } from "#/context/files";
import { ChatInterface } from "../components/features/chat/chat-interface";
import { WsClientProvider } from "#/context/ws-client-provider";
import { EventHandler } from "../wrapper/event-handler";
@@ -50,6 +52,7 @@ function AppContent() {
const { initialPrompt, files } = useSelector(
(state: RootState) => state.initialQuery,
);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
const endSession = useEndSession();
@@ -134,9 +137,37 @@ function AppContent() {
icon: <DiGit className="w-6 h-6" />,
},
{
label: t(I18nKey.WORKSPACE$TITLE),
to: "workspace",
icon: <CodeIcon />,
label: (
<div className="flex items-center gap-1">
{t(I18nKey.VSCODE$TITLE)}
</div>
),
to: "vscode",
icon: <VscCode className="w-5 h-5" />,
rightContent: !RUNTIME_INACTIVE_STATES.includes(
curAgentState,
) ? (
<FaExternalLinkAlt
className="w-3 h-3 text-neutral-400 cursor-pointer"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
if (conversationId) {
try {
const response = await fetch(
`/api/conversations/${conversationId}/vscode-url`,
);
const data = await response.json();
if (data.vscode_url) {
window.open(data.vscode_url, "_blank");
}
} catch (err) {
// Silently handle the error
}
}
}}
/>
) : null,
},
{
label: t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL),
@@ -160,9 +191,7 @@ function AppContent() {
},
]}
>
<FilesProvider>
<Outlet />
</FilesProvider>
<Outlet />
</Container>
}
/>
-57
View File
@@ -1,57 +0,0 @@
import React from "react";
import { useRouteError } from "react-router";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useTranslation } from "react-i18next";
import { FileExplorer } from "#/components/features/file-explorer/file-explorer";
import { useFiles } from "#/context/files";
import { getLanguageFromPath } from "#/utils/get-language-from-path";
export function ErrorBoundary() {
const error = useRouteError();
const { t } = useTranslation();
return (
<div className="w-full h-full border border-danger rounded-b-xl flex flex-col items-center justify-center gap-2 bg-red-500/5">
<h1 className="text-3xl font-bold">{t("ERROR$GENERIC")}</h1>
{error instanceof Error && <pre>{error.message}</pre>}
</div>
);
}
function FileViewer() {
const [fileExplorerIsOpen, setFileExplorerIsOpen] = React.useState(true);
const { selectedPath, files } = useFiles();
const toggleFileExplorer = () => {
setFileExplorerIsOpen((prev) => !prev);
};
return (
<div className="flex h-full bg-base-secondary relative">
<FileExplorer isOpen={fileExplorerIsOpen} onToggle={toggleFileExplorer} />
<div className="w-full h-full flex flex-col">
{selectedPath && files[selectedPath] && (
<div className="h-full w-full overflow-auto">
<SyntaxHighlighter
language={getLanguageFromPath(selectedPath)}
style={vscDarkPlus}
customStyle={{
margin: 0,
padding: "10px",
height: "100%",
background: "#171717",
fontSize: "0.875rem",
borderRadius: 0,
}}
>
{files[selectedPath]}
</SyntaxHighlighter>
</div>
)}
</div>
</div>
);
}
export default FileViewer;
+81
View File
@@ -0,0 +1,81 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { useConversation } from "#/context/conversation-context";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
function VSCodeTab() {
const { t } = useTranslation();
const { conversationId } = useConversation();
const [vsCodeUrl, setVsCodeUrl] = React.useState<string | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
React.useEffect(() => {
async function fetchVSCodeUrl() {
if (!conversationId || isRuntimeInactive) return;
try {
setIsLoading(true);
const response = await fetch(
`/api/conversations/${conversationId}/vscode-url`,
);
const data = await response.json();
if (data.vscode_url) {
setVsCodeUrl(data.vscode_url);
} else {
setError(t(I18nKey.VSCODE$URL_NOT_AVAILABLE));
}
} catch (err) {
setError(t(I18nKey.VSCODE$FETCH_ERROR));
// Error is handled by setting the error state
} finally {
setIsLoading(false);
}
}
fetchVSCodeUrl();
}, [conversationId, isRuntimeInactive, t]);
if (isRuntimeInactive) {
return (
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
</div>
);
}
if (isLoading) {
return (
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
</div>
);
}
if (error || !vsCodeUrl) {
return (
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
{error || t(I18nKey.VSCODE$URL_NOT_AVAILABLE)}
</div>
);
}
return (
<div className="h-full w-full">
<iframe
title={t(I18nKey.VSCODE$TITLE)}
src={vsCodeUrl}
className="w-full h-full border-0"
allow={t(I18nKey.VSCODE$IFRAME_PERMISSIONS)}
/>
</div>
);
}
export default VSCodeTab;
+4 -4
View File
@@ -1,18 +1,18 @@
enum TabOption {
PLANNER = "planner",
CODE = "code",
BROWSER = "browser",
JUPYTER = "jupyter",
VSCODE = "vscode",
}
type TabType =
| TabOption.PLANNER
| TabOption.CODE
| TabOption.BROWSER
| TabOption.JUPYTER;
| TabOption.JUPYTER
| TabOption.VSCODE;
const AllTabs = [
TabOption.CODE,
TabOption.VSCODE,
TabOption.BROWSER,
TabOption.PLANNER,
TabOption.JUPYTER,
+11 -41
View File
@@ -15,11 +15,11 @@ This directory (`OpenHands/microagents/`) contains shareable microagents that ar
Directory structure:
```
OpenHands/microagents/
├── knowledge/ # Keyword-triggered expertise
│ ├── git.md # Git operations
│ ├── testing.md # Testing practices
│ └── docker.md # Docker guidelines
└── tasks/ # Interactive workflows
├── # Keyword-triggered expertise
│ ├── git.md # Git operations
│ ├── testing.md # Testing practices
│ └── docker.md # Docker guidelines
└── # These microagents are always loaded
├── pr_review.md # PR review process
├── bug_fix.md # Bug fixing workflow
└── feature.md # Feature implementation
@@ -37,8 +37,7 @@ your-repository/
└── .openhands/
└── microagents/
└── repo.md # Repository-specific instructions
└── knowledges/ # Private micro-agents that are only available inside this repo
└── tasks/ # Private micro-agents that are only available inside this repo
└── ... # Private micro-agents that are only available inside this repo
```
@@ -47,7 +46,6 @@ your-repository/
When OpenHands works with a repository, it:
1. Loads repository-specific instructions from `.openhands/microagents/repo.md` if present
2. Loads relevant knowledge agents based on keywords in conversations
3. Enable task agent if user select one of them
## Types of Microagents
@@ -68,7 +66,7 @@ Key characteristics:
- **Reusable**: Knowledge can be applied across multiple projects
- **Versioned**: Support multiple versions of tools/frameworks
You can see an example of a knowledge-based agent in [OpenHands's github microagent](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/github.md).
You can see an example of a knowledge-based agent in [OpenHands's github microagent](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/github.md).
### 2. Repository Agents
@@ -86,22 +84,6 @@ Key features:
You can see an example of a repo agent in [the agent for the OpenHands repo itself](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md).
### 3. Task Agents
Task agents provide interactive workflows that guide users through common development tasks. They:
- Accept user inputs
- Follow predefined steps
- Adapt to context
- Provide consistent results
Key capabilities:
- **Interactive**: Guide users through complex processes
- **Validating**: Check inputs and conditions
- **Flexible**: Adapt to different scenarios
- **Reproducible**: Ensure consistent outcomes
Example workflow:
You can see an example of a task-based agent in [OpenHands's pull request updating microagent](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks/update_pr_description.md).
## Contributing
@@ -113,13 +95,8 @@ You can see an example of a task-based agent in [OpenHands's pull request updati
- Common problem solutions
- General development guidelines
2. **Task Agents** - When you have:
- Repeatable workflows
- Multi-step processes
- Common development tasks
- Standard procedures
3. **Repository Agents** - When you need:
2. **Repository Agents** - When you need:
- Project-specific guidelines
- Team conventions and practices
- Custom workflow documentation
@@ -134,14 +111,8 @@ You can see an example of a task-based agent in [OpenHands's pull request updati
- Use file patterns when relevant
- Keep knowledge general and reusable
2. **For Task Agents**:
- Break workflows into clear steps
- Validate user inputs
- Provide helpful defaults
- Include usage examples
- Make steps adaptable
3. **For Repository Agents**:
2. **For Repository Agents**:
- Document clear setup instructions
- Include repository structure details
- Specify testing and build procedures
@@ -152,9 +123,8 @@ You can see an example of a task-based agent in [OpenHands's pull request updati
### Submission Process
1. Create your agent file in the appropriate directory:
- `knowledge/` for expertise (public, shareable)
- `tasks/` for workflows (public, shareable)
- Note: Repository agents should remain in their respective repositories' `.openhands/microagents/` directory
- `microagents/` for expertise (public, shareable)
- Note: Repository-specific agents should remain in their respective repositories' `.openhands/microagents/` directory
2. Test thoroughly
3. Submit a pull request to OpenHands
@@ -38,4 +38,4 @@ For detailed information, see:
- [Microagents Overview](https://docs.all-hands.dev/modules/usage/prompting/microagents-overview)
- [Microagents Syntax](https://docs.all-hands.dev/modules/usage/prompting/microagents-syntax)
- [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/knowledge/github.md)
- [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/github.md)
@@ -1,6 +1,5 @@
---
name: add_openhands_repo_instruction
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
@@ -1,6 +1,5 @@
---
name: address_pr_comments
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
@@ -1,6 +1,5 @@
---
name: get_test_to_pass
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
@@ -1,6 +1,5 @@
---
name: update_pr_description
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
@@ -1,6 +1,5 @@
---
name: update_test_for_new_implementation
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
+152 -656
View File
@@ -1,32 +1,37 @@
import asyncio
import logging
import sys
import time
from pathlib import Path
from typing import List, Optional
from uuid import uuid4
import toml
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.application import Application
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.formatted_text import HTML, FormattedText
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.shortcuts import clear, print_container
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
from prompt_toolkit.shortcuts import clear
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands import __version__
from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.core.cli_commands import (
check_folder_security_agreement,
handle_commands,
)
from openhands.core.cli_tui import (
UsageMetrics,
display_banner,
display_event,
display_initial_user_prompt,
display_initialization_animation,
display_runtime_initialization_message,
display_welcome_message,
read_confirmation_input,
read_prompt_input,
)
from openhands.core.cli_utils import (
update_usage_metrics,
)
from openhands.core.config import (
AppConfig,
parse_arguments,
setup_config_from_args,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.logger import openhands_logger as logger
from openhands.core.loop import run_agent_until_done
from openhands.core.schema import AgentState
@@ -39,634 +44,64 @@ from openhands.core.setup import (
)
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import (
Action,
ActionConfirmationStatus,
ChangeAgentStateAction,
CmdRunAction,
FileEditAction,
MessageAction,
)
from openhands.events.event import Event
from openhands.events.observation import (
AgentStateChangedObservation,
CmdOutputObservation,
FileEditObservation,
FileReadObservation,
)
from openhands.io import read_task
from openhands.llm.metrics import Metrics
from openhands.mcp import fetch_mcp_tools_from_config
from openhands.microagent.microagent import BaseMicroagent
# Color and styling constants
COLOR_GOLD = '#FFD700'
COLOR_GREY = '#808080'
DEFAULT_STYLE = Style.from_dict(
{
'gold': COLOR_GOLD,
'grey': COLOR_GREY,
'prompt': f'{COLOR_GOLD} bold',
}
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
LLMSummarizingCondenserConfig,
)
COMMANDS = {
'/exit': 'Exit the application',
'/help': 'Display available commands',
'/init': 'Initialize a new repository',
}
REPO_MD_CREATE_PROMPT = """
Please explore this repository. Create the file .openhands/microagents/repo.md with:
- A description of the project
- An overview of the file structure
- Any information on how to run tests or other relevant commands
- Any other information that would be helpful to a brand new developer
Keep it short--just a few paragraphs will do.
"""
from openhands.microagent.microagent import BaseMicroagent
from openhands.runtime.base import Runtime
from openhands.storage.settings.file_settings_store import FileSettingsStore
class CommandCompleter(Completer):
"""Custom completer for commands."""
def get_completions(self, document, complete_event):
text = document.text
# Only show completions if the user has typed '/'
if text.startswith('/'):
# If just '/' is typed, show all commands
if text == '/':
for command, description in COMMANDS.items():
yield Completion(
command[1:], # Remove the leading '/' as it's already typed
start_position=0,
display=f'{command} - {description}',
)
# Otherwise show matching commands
else:
for command, description in COMMANDS.items():
if command.startswith(text):
yield Completion(
command[len(text) :], # Complete the remaining part
start_position=0,
display=f'{command} - {description}',
)
class UsageMetrics:
def __init__(self):
self.total_cost: float = 0.00
self.total_input_tokens: int = 0
self.total_output_tokens: int = 0
self.total_cache_read: int = 0
self.total_cache_write: int = 0
prompt_session = PromptSession(style=DEFAULT_STYLE, completer=CommandCompleter())
def display_message(message: str):
message = message.strip()
if message:
print_formatted_text(f'\n{message}\n')
def display_command(command: str):
container = Frame(
TextArea(
text=command,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Command Run',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_confirmation(confirmation_state: ActionConfirmationStatus):
status_map = {
ActionConfirmationStatus.CONFIRMED: ('ansigreen', ''),
ActionConfirmationStatus.REJECTED: ('ansired', ''),
ActionConfirmationStatus.AWAITING_CONFIRMATION: ('ansiyellow', ''),
}
color, icon = status_map.get(confirmation_state, ('ansiyellow', ''))
print_formatted_text(
FormattedText(
[
(color, f'{icon} '),
(color, str(confirmation_state)),
('', '\n'),
]
)
)
def display_command_output(output: str):
lines = output.split('\n')
formatted_lines = []
for line in lines:
if line.startswith('[Python Interpreter') or line.startswith('openhands@'):
# TODO: clean this up once we clean up terminal output
continue
formatted_lines.append(line)
formatted_lines.append('\n')
# Remove the last newline if it exists
if formatted_lines:
formatted_lines.pop()
container = Frame(
TextArea(
text=''.join(formatted_lines),
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Command Output',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_file_edit(event: FileEditAction | FileEditObservation):
container = Frame(
TextArea(
text=f'{event}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='File Edit',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_file_read(event: FileReadObservation):
container = Frame(
TextArea(
text=f'{event}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='File Read',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_event(event: Event, config: AppConfig) -> None:
if isinstance(event, Action):
if hasattr(event, 'thought'):
display_message(event.thought)
if isinstance(event, MessageAction):
if event.source == EventSource.AGENT:
display_message(event.content)
if isinstance(event, CmdRunAction):
display_command(event.command)
if isinstance(event, CmdOutputObservation):
display_command_output(event.content)
if isinstance(event, FileEditAction):
display_file_edit(event)
if isinstance(event, FileEditObservation):
display_file_edit(event)
if isinstance(event, FileReadObservation):
display_file_read(event)
if hasattr(event, 'confirmation_state') and config.security.confirmation_mode:
display_confirmation(event.confirmation_state)
def display_help(style=DEFAULT_STYLE):
print_formatted_text(
HTML(f'\n<grey>OpenHands CLI v{__version__}</grey>\n'), style=style
)
print_formatted_text(
HTML(
'<gold>OpenHands CLI lets you interact with the OpenHands agent from the command line.</gold>'
)
)
print_formatted_text('')
print_formatted_text('Things that you can try:')
print_formatted_text(
HTML('• Ask questions about the codebase <grey>> How does main.py work?</grey>')
)
print_formatted_text(
HTML(
'• Edit files or add new features <grey>> Add a new function to ...</grey>'
)
)
print_formatted_text(
HTML('• Find and fix issues <grey>> Fix the type error in ...</grey>')
)
print_formatted_text('')
print_formatted_text('Some tips to get the most out of OpenHands:')
print_formatted_text(
'• Be as specific as possible about the desired outcome or the problem to be solved.'
)
print_formatted_text(
'• Provide context, including relevant file paths and line numbers if available.'
)
print_formatted_text('• Break large tasks into smaller, manageable prompts.')
print_formatted_text('• Include relevant error messages or logs.')
print_formatted_text(
'• Specify the programming language or framework, if not obvious.'
)
print_formatted_text('')
print_formatted_text(HTML('Interactive commands:'), style=style)
for command, description in COMMANDS.items():
print_formatted_text(
HTML(f'<gold><b>{command}</b></gold> - <grey>{description}</grey>'),
style=style,
)
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>Learn more at: https://docs.all-hands.dev/modules/usage/getting-started</grey>'
)
)
print_formatted_text('')
def display_banner(session_id: str, is_loaded: asyncio.Event):
print_formatted_text(
HTML(r"""<gold>
___ _ _ _
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|_|
</gold>"""),
style=DEFAULT_STYLE,
)
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
banner_text = (
'Initialized session' if is_loaded.is_set() else 'Initializing session'
)
print_formatted_text(HTML(f'\n<grey>{banner_text} {session_id}</grey>\n'))
def display_welcome_message():
print_formatted_text(
HTML("<gold>Let's start building!</gold>\n"), style=DEFAULT_STYLE
)
print_formatted_text(
HTML('What do you want to build? <grey>Type /help for help</grey>\n'),
style=DEFAULT_STYLE,
)
def display_initialization_animation(text, is_loaded: asyncio.Event):
ANIMATION_FRAMES = ['', '', '', '', '', '', '', '', '', '']
i = 0
while not is_loaded.is_set():
sys.stdout.write('\n')
sys.stdout.write(
f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
)
sys.stdout.flush()
time.sleep(0.1)
i += 1
sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
sys.stdout.flush()
async def read_prompt_input(multiline=False):
async def cleanup_session(
loop: asyncio.AbstractEventLoop,
agent: Agent,
runtime: Runtime,
controller: AgentController,
):
"""Clean up all resources from the current session."""
try:
if multiline:
kb = KeyBindings()
# Cancel all running tasks except the current one
current_task = asyncio.current_task(loop)
pending = [task for task in asyncio.all_tasks(loop) if task is not current_task]
for task in pending:
task.cancel()
@kb.add('c-d')
def _(event):
event.current_buffer.validate_and_handle()
# Wait for all tasks to complete with a timeout
if pending:
await asyncio.wait(pending, timeout=5.0)
with patch_stdout():
message = await prompt_session.prompt_async(
'Enter your message and press Ctrl+D to finish:\n',
multiline=True,
key_bindings=kb,
)
else:
with patch_stdout():
message = await prompt_session.prompt_async(
'> ',
)
return message
except KeyboardInterrupt:
return '/exit'
except EOFError:
return '/exit'
# Reset agent, close runtime and controller
agent.reset()
runtime.close()
await controller.close()
except Exception as e:
logger.error(f'Error during session cleanup: {e}')
async def read_confirmation_input():
try:
confirmation = await prompt_session.prompt_async(
'Confirm action (possible security risk)? (y/n) > ',
)
return confirmation.lower() == 'y'
except (KeyboardInterrupt, EOFError):
return False
async def init_repository(current_dir: str) -> bool:
repo_file_path = Path(current_dir) / '.openhands' / 'microagents' / 'repo.md'
init_repo = False
if repo_file_path.exists():
try:
content = await asyncio.get_event_loop().run_in_executor(
None, read_file, repo_file_path
)
print_formatted_text(
'Repository instructions file (repo.md) already exists.\n'
)
container = Frame(
TextArea(
text=content,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Repository Instructions (repo.md)',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('') # Add a newline after the frame
init_repo = cli_confirm(
'Do you want to re-initialize?',
['Yes, re-initialize', 'No, dismiss'],
)
if init_repo:
write_to_file(repo_file_path, '')
except Exception:
print_formatted_text('Error reading repository instructions file (repo.md)')
init_repo = False
else:
print_formatted_text(
'\nRepository instructions file will be created by exploring the repository.\n'
)
init_repo = cli_confirm(
'Do you want to proceed?',
['Yes, create', 'No, dismiss'],
)
return init_repo
def read_file(file_path):
with open(file_path, 'r') as f:
return f.read()
def write_to_file(file_path, content):
with open(file_path, 'w') as f:
f.write(content)
def cli_confirm(question: str = 'Are you sure?', choices: Optional[List[str]] = None):
if choices is None:
choices = ['Yes', 'No']
selected = [0] # Using list to allow modification in closure
def get_choice_text():
return [
('class:question', f'{question}\n\n'),
] + [
(
'class:selected' if i == selected[0] else 'class:unselected',
f"{'> ' if i == selected[0] else ' '}{choice}\n",
)
for i, choice in enumerate(choices)
]
kb = KeyBindings()
@kb.add('up')
def _(event):
selected[0] = (selected[0] - 1) % len(choices)
@kb.add('down')
def _(event):
selected[0] = (selected[0] + 1) % len(choices)
@kb.add('enter')
def _(event):
event.app.exit(result=selected[0] == 0)
style = Style.from_dict({'selected': COLOR_GOLD, 'unselected': ''})
layout = Layout(
HSplit(
[
Window(
FormattedTextControl(get_choice_text),
always_hide_cursor=True,
)
]
)
)
app = Application(
layout=layout,
key_bindings=kb,
style=style,
mouse_support=True,
full_screen=False,
)
return app.run(in_thread=True)
def update_usage_metrics(event: Event, usage_metrics: UsageMetrics):
"""Updates the UsageMetrics object with data from an event's llm_metrics."""
if hasattr(event, 'llm_metrics'):
llm_metrics: Metrics | None = getattr(event, 'llm_metrics', None)
if llm_metrics:
# Safely get accumulated_cost
cost = getattr(llm_metrics, 'accumulated_cost', 0)
# Ensure cost is a number before adding
usage_metrics.total_cost += cost if isinstance(cost, float) else 0
# Safely get token usage details object/dict
token_usage = getattr(llm_metrics, 'accumulated_token_usage', None)
if token_usage:
# Assume object access using getattr, providing defaults
prompt_tokens = getattr(token_usage, 'prompt_tokens', 0)
completion_tokens = getattr(token_usage, 'completion_tokens', 0)
cache_read = getattr(token_usage, 'cache_read_tokens', 0)
cache_write = getattr(token_usage, 'cache_write_tokens', 0)
# Ensure tokens are numbers before adding
usage_metrics.total_input_tokens += (
prompt_tokens if isinstance(prompt_tokens, int) else 0
)
usage_metrics.total_output_tokens += (
completion_tokens if isinstance(completion_tokens, int) else 0
)
usage_metrics.total_cache_read += (
cache_read if isinstance(cache_read, int) else 0
)
usage_metrics.total_cache_write += (
cache_write if isinstance(cache_write, int) else 0
)
def shutdown(usage_metrics: UsageMetrics, session_id: str):
cost_str = f'${usage_metrics.total_cost:.6f}'
input_tokens_str = f'{usage_metrics.total_input_tokens:,}'
cache_read_str = f'{usage_metrics.total_cache_read:,}'
cache_write_str = f'{usage_metrics.total_cache_write:,}'
output_tokens_str = f'{usage_metrics.total_output_tokens:,}'
total_tokens_str = (
f'{usage_metrics.total_input_tokens + usage_metrics.total_output_tokens:,}'
)
labels_and_values = [
(' Total Cost (USD):', cost_str),
(' Total Input Tokens:', input_tokens_str),
(' Cache Hits:', cache_read_str),
(' Cache Writes:', cache_write_str),
(' Total Output Tokens:', output_tokens_str),
(' Total Tokens:', total_tokens_str),
]
# Calculate max widths for alignment
max_label_width = max(len(label) for label, _ in labels_and_values)
max_value_width = max(len(value) for _, value in labels_and_values)
# Construct the summary text with aligned columns
summary_lines = [
f'{label:<{max_label_width}} {value:>{max_value_width}}'
for label, value in labels_and_values
]
summary_text = '\n'.join(summary_lines)
container = Frame(
TextArea(
text=summary_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Session Summary',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text(HTML(f'\n<grey>Closed session {session_id}</grey>\n'))
def manage_openhands_file(folder_path=None, add_to_trusted=False):
openhands_file = Path.home() / '.openhands.toml'
default_content: dict = {'trusted_dirs': []}
if not openhands_file.exists():
with open(openhands_file, 'w') as f:
toml.dump(default_content, f)
if folder_path:
with open(openhands_file, 'r') as f:
try:
config = toml.load(f)
except Exception:
config = default_content
if 'trusted_dirs' not in config:
config['trusted_dirs'] = []
if folder_path in config['trusted_dirs']:
return True
if add_to_trusted:
config['trusted_dirs'].append(folder_path)
with open(openhands_file, 'w') as f:
toml.dump(config, f)
return False
return False
def check_folder_security_agreement(current_dir):
is_trusted = manage_openhands_file(current_dir)
if not is_trusted:
security_frame = Frame(
TextArea(
text=(
f'Do you trust the files in this folder?\n\n'
f'{current_dir}\n\n'
'OpenHands may read and execute files in this folder with your permission.'
),
style=COLOR_GREY,
read_only=True,
wrap_lines=True,
),
style=f'fg:{COLOR_GREY}',
)
clear()
print_container(security_frame)
confirm = cli_confirm('Do you wish to continue?', ['Yes, proceed', 'No, exit'])
if confirm:
manage_openhands_file(current_dir, add_to_trusted=True)
return confirm
return True
async def main(loop: asyncio.AbstractEventLoop):
"""Runs the agent in CLI mode."""
async def run_session(
loop: asyncio.AbstractEventLoop,
config: AppConfig,
settings_store: FileSettingsStore,
current_dir: str,
initial_user_action: str | None = None,
) -> bool:
reload_microagents = False
args = parse_arguments()
logger.setLevel(logging.WARNING)
# Load config from toml and override with command line arguments
config: AppConfig = setup_config_from_args(args)
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base
if not current_dir:
raise ValueError('Workspace base directory not specified')
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)
# If we have a task, create initial user action
initial_user_action = MessageAction(content=task_str) if task_str else None
new_session_requested = False
sid = str(uuid4())
is_loaded = asyncio.Event()
# Show OpenHands banner and session ID
display_banner(session_id=sid, is_loaded=is_loaded)
# Show runtime initialization message
display_runtime_initialization_message(config.runtime)
# Show Initialization loader
loop.run_in_executor(
@@ -690,41 +125,29 @@ async def main(loop: asyncio.AbstractEventLoop):
usage_metrics = UsageMetrics()
async def prompt_for_next_task():
nonlocal reload_microagents
nonlocal reload_microagents, new_session_requested
while True:
next_message = await read_prompt_input(config.cli_multiline_input)
if not next_message.strip():
continue
if next_message == '/exit':
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
)
shutdown(usage_metrics, sid)
return
elif next_message == '/help':
display_help()
continue
elif next_message == '/init':
if config.runtime == 'local':
init_repo = await init_repository(current_dir)
if init_repo:
event_stream.add_event(
MessageAction(content=REPO_MD_CREATE_PROMPT),
EventSource.USER,
)
reload_microagents = True
return
else:
print_formatted_text(
'\nRepository initialization through the CLI is only supported for local runtime.\n'
)
continue
(
close_repl,
reload_microagents,
new_session_requested,
) = await handle_commands(
next_message,
event_stream,
usage_metrics,
sid,
config,
current_dir,
settings_store,
)
action = MessageAction(content=next_message)
event_stream.add_event(action, EventSource.USER)
return
if close_repl:
return
async def on_event_async(event: Event) -> None:
nonlocal reload_microagents
@@ -785,10 +208,6 @@ async def main(loop: asyncio.AbstractEventLoop):
# Clear loading animation
is_loaded.set()
if not check_folder_security_agreement(current_dir):
# User rejected, exit application
return
# Clear the terminal
clear()
@@ -800,7 +219,10 @@ async def main(loop: asyncio.AbstractEventLoop):
if initial_user_action:
# If there's an initial user action, enqueue it and do not prompt again
event_stream.add_event(initial_user_action, EventSource.USER)
display_initial_user_prompt(initial_user_action)
event_stream.add_event(
MessageAction(content=initial_user_action), EventSource.USER
)
else:
# Otherwise prompt for the user's first message right away
asyncio.create_task(prompt_for_next_task())
@@ -809,6 +231,79 @@ async def main(loop: asyncio.AbstractEventLoop):
controller, runtime, memory, [AgentState.STOPPED, AgentState.ERROR]
)
await cleanup_session(loop, agent, runtime, controller)
return new_session_requested
async def main(loop: asyncio.AbstractEventLoop):
"""Runs the agent in CLI mode."""
args = parse_arguments()
logger.setLevel(logging.WARNING)
# Load config from toml and override with command line arguments
config: AppConfig = setup_config_from_args(args)
# Load settings from Settings Store
# TODO: Make this generic?
settings_store = await FileSettingsStore.get_instance(config=config, user_id=None)
settings = await settings_store.load()
# Use settings from settings store if available and override with command line arguments
if settings:
config.default_agent = args.agent_cls if args.agent_cls else settings.agent
if not args.llm_config and settings.llm_model and settings.llm_api_key:
llm_config = config.get_llm_config()
llm_config.model = settings.llm_model
llm_config.api_key = settings.llm_api_key
llm_config.base_url = settings.llm_base_url
config.set_llm_config(llm_config)
config.security.confirmation_mode = (
settings.confirmation_mode if settings.confirmation_mode else False
)
if settings.enable_default_condenser:
# TODO: Make this generic?
llm_config = config.get_llm_config()
agent_config = config.get_agent_config(config.default_agent)
agent_config.condenser = LLMSummarizingCondenserConfig(
llm_config=llm_config,
type='llm',
)
config.set_agent_config(agent_config)
config.enable_default_condenser = True
else:
agent_config = config.get_agent_config(config.default_agent)
agent_config.condenser = NoOpCondenserConfig(type='noop')
config.set_agent_config(agent_config)
config.enable_default_condenser = False
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base
if not current_dir:
raise ValueError('Workspace base directory not specified')
if not check_folder_security_agreement(config, current_dir):
# User rejected, exit application
return
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)
# Run the first session
new_session_requested = await run_session(
loop, config, settings_store, current_dir, task_str
)
# If a new session was requested, run it
while new_session_requested:
new_session_requested = await run_session(
loop, config, settings_store, current_dir, None
)
if __name__ == '__main__':
loop = asyncio.new_event_loop()
@@ -829,6 +324,7 @@ if __name__ == '__main__':
pending = asyncio.all_tasks(loop)
for task in pending:
task.cancel()
# Wait for all tasks to complete with a timeout
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
loop.close()
+283
View File
@@ -0,0 +1,283 @@
import asyncio
from pathlib import Path
from prompt_toolkit import print_formatted_text
from prompt_toolkit.shortcuts import clear, print_container
from prompt_toolkit.widgets import Frame, TextArea
from openhands.core.cli_settings import (
display_settings,
modify_llm_settings_advanced,
modify_llm_settings_basic,
)
from openhands.core.cli_tui import (
COLOR_GREY,
UsageMetrics,
cli_confirm,
display_help,
display_shutdown_message,
display_status,
)
from openhands.core.cli_utils import (
add_local_config_trusted_dir,
get_local_config_trusted_dirs,
read_file,
write_to_file,
)
from openhands.core.config import (
AppConfig,
)
from openhands.core.schema import AgentState
from openhands.events import EventSource
from openhands.events.action import (
ChangeAgentStateAction,
MessageAction,
)
from openhands.events.stream import EventStream
from openhands.storage.settings.file_settings_store import FileSettingsStore
async def handle_commands(
command: str,
event_stream: EventStream,
usage_metrics: UsageMetrics,
sid: str,
config: AppConfig,
current_dir: str,
settings_store: FileSettingsStore,
) -> tuple[bool, bool, bool]:
close_repl = False
reload_microagents = False
new_session_requested = False
if command == '/exit':
close_repl = handle_exit_command(
event_stream,
usage_metrics,
sid,
)
elif command == '/help':
handle_help_command()
elif command == '/init':
close_repl, reload_microagents = await handle_init_command(
config, event_stream, current_dir
)
elif command == '/status':
handle_status_command(usage_metrics, sid)
elif command == '/new':
close_repl, new_session_requested = handle_new_command(
event_stream, usage_metrics, sid
)
elif command == '/settings':
await handle_settings_command(config, settings_store)
else:
close_repl = True
action = MessageAction(content=command)
event_stream.add_event(action, EventSource.USER)
return close_repl, reload_microagents, new_session_requested
def handle_exit_command(
event_stream: EventStream, usage_metrics: UsageMetrics, sid: str
) -> bool:
close_repl = False
confirm_exit = (
cli_confirm('\nTerminate session?', ['Yes, proceed', 'No, dismiss']) == 0
)
if confirm_exit:
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED),
EventSource.ENVIRONMENT,
)
display_shutdown_message(usage_metrics, sid)
close_repl = True
return close_repl
def handle_help_command():
display_help()
async def handle_init_command(
config: AppConfig, event_stream: EventStream, current_dir: str
) -> tuple[bool, bool]:
REPO_MD_CREATE_PROMPT = """
Please explore this repository. Create the file .openhands/microagents/repo.md with:
- A description of the project
- An overview of the file structure
- Any information on how to run tests or other relevant commands
- Any other information that would be helpful to a brand new developer
Keep it short--just a few paragraphs will do.
"""
close_repl = False
reload_microagents = False
if config.runtime == 'local':
init_repo = await init_repository(current_dir)
if init_repo:
event_stream.add_event(
MessageAction(content=REPO_MD_CREATE_PROMPT),
EventSource.USER,
)
reload_microagents = True
close_repl = True
else:
print_formatted_text(
'\nRepository initialization through the CLI is only supported for local runtime.\n'
)
return close_repl, reload_microagents
def handle_status_command(usage_metrics: UsageMetrics, sid: str):
display_status(usage_metrics, sid)
def handle_new_command(
event_stream: EventStream, usage_metrics: UsageMetrics, sid: str
) -> tuple[bool, bool]:
close_repl = False
new_session_requested = False
new_session_requested = (
cli_confirm(
'\nCurrent session will be terminated and you will lose the conversation history.\n\nContinue?',
['Yes, proceed', 'No, dismiss'],
)
== 0
)
if new_session_requested:
close_repl = True
new_session_requested = True
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED),
EventSource.ENVIRONMENT,
)
display_shutdown_message(usage_metrics, sid)
return close_repl, new_session_requested
async def handle_settings_command(
config: AppConfig,
settings_store: FileSettingsStore,
):
display_settings(config)
modify_settings = cli_confirm(
'\nWhich settings would you like to modify?',
[
'Basic',
'Advanced',
'Go back',
],
)
if modify_settings == 0:
await modify_llm_settings_basic(config, settings_store)
elif modify_settings == 1:
await modify_llm_settings_advanced(config, settings_store)
async def init_repository(current_dir: str) -> bool:
repo_file_path = Path(current_dir) / '.openhands' / 'microagents' / 'repo.md'
init_repo = False
if repo_file_path.exists():
try:
content = await asyncio.get_event_loop().run_in_executor(
None, read_file, repo_file_path
)
print_formatted_text(
'Repository instructions file (repo.md) already exists.\n'
)
container = Frame(
TextArea(
text=content,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Repository Instructions (repo.md)',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('') # Add a newline after the frame
init_repo = (
cli_confirm(
'Do you want to re-initialize?',
['Yes, re-initialize', 'No, dismiss'],
)
== 0
)
if init_repo:
write_to_file(repo_file_path, '')
except Exception:
print_formatted_text('Error reading repository instructions file (repo.md)')
init_repo = False
else:
print_formatted_text(
'\nRepository instructions file will be created by exploring the repository.\n'
)
init_repo = (
cli_confirm(
'Do you want to proceed?',
['Yes, create', 'No, dismiss'],
)
== 0
)
return init_repo
def check_folder_security_agreement(config: AppConfig, current_dir):
# Directories trusted by user for the CLI to use as workspace
# Config from ~/.openhands/config.toml overrides the app config
app_config_trusted_dirs = config.sandbox.trusted_dirs
local_config_trusted_dirs = get_local_config_trusted_dirs()
trusted_dirs = local_config_trusted_dirs
if not local_config_trusted_dirs:
trusted_dirs = app_config_trusted_dirs
is_trusted = current_dir in trusted_dirs
if not is_trusted:
security_frame = Frame(
TextArea(
text=(
f' Do you trust the files in this folder?\n\n'
f' {current_dir}\n\n'
' OpenHands may read and execute files in this folder with your permission.'
),
style=COLOR_GREY,
read_only=True,
wrap_lines=True,
),
style=f'fg:{COLOR_GREY}',
)
clear()
print_container(security_frame)
print_formatted_text('')
confirm = (
cli_confirm('Do you wish to continue?', ['Yes, proceed', 'No, exit']) == 0
)
if confirm:
add_local_config_trusted_dir(current_dir)
return confirm
return True
+348
View File
@@ -0,0 +1,348 @@
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.completion import FuzzyWordCompleter
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.shortcuts import print_container
from prompt_toolkit.widgets import Frame, TextArea
from pydantic import SecretStr
from openhands.controller.agent import Agent
from openhands.core.cli_tui import (
COLOR_GREY,
UserCancelledError,
cli_confirm,
kb_cancel,
)
from openhands.core.cli_utils import (
VERIFIED_ANTHROPIC_MODELS,
VERIFIED_OPENAI_MODELS,
VERIFIED_PROVIDERS,
organize_models_and_providers,
)
from openhands.core.config import AppConfig
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import OH_DEFAULT_AGENT
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
LLMSummarizingCondenserConfig,
)
from openhands.storage.data_models.settings import Settings
from openhands.storage.settings.file_settings_store import FileSettingsStore
from openhands.utils.llm import get_supported_llm_models
def display_settings(config: AppConfig):
llm_config = config.get_llm_config()
advanced_llm_settings = True if llm_config.base_url else False
# Prepare labels and values based on settings
labels_and_values = []
if not advanced_llm_settings:
# Attempt to determine provider, fallback if not directly available
provider = getattr(
llm_config,
'provider',
llm_config.model.split('/')[0] if '/' in llm_config.model else 'Unknown',
)
labels_and_values.extend(
[
(' LLM Provider', str(provider)),
(' LLM Model', str(llm_config.model)),
(' API Key', '********' if llm_config.api_key else 'Not Set'),
]
)
else:
labels_and_values.extend(
[
(' Custom Model', str(llm_config.model)),
(' Base URL', str(llm_config.base_url)),
(' API Key', '********' if llm_config.api_key else 'Not Set'),
]
)
# Common settings
labels_and_values.extend(
[
(' Agent', str(config.default_agent)),
(
' Confirmation Mode',
'Enabled' if config.security.confirmation_mode else 'Disabled',
),
(
' Memory Condensation',
'Enabled' if config.enable_default_condenser else 'Disabled',
),
]
)
# Calculate max widths for alignment
# Ensure values are strings for len() calculation
str_labels_and_values = [(label, str(value)) for label, value in labels_and_values]
max_label_width = (
max(len(label) for label, _ in str_labels_and_values)
if str_labels_and_values
else 0
)
# Construct the summary text with aligned columns
settings_lines = [
f'{label+":":<{max_label_width+1}} {value:<}' # Changed value alignment to left (<)
for label, value in str_labels_and_values
]
settings_text = '\n'.join(settings_lines)
container = Frame(
TextArea(
text=settings_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Settings',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
async def get_validated_input(
session: PromptSession,
prompt_text: str,
completer=None,
validator=None,
error_message='Input cannot be empty',
):
session.completer = completer
value = None
while True:
value = await session.prompt_async(prompt_text)
if validator:
is_valid = validator(value)
if not is_valid:
print_formatted_text('')
print_formatted_text(HTML(f'<grey>{error_message}: {value}</grey>'))
print_formatted_text('')
continue
elif not value:
print_formatted_text('')
print_formatted_text(HTML(f'<grey>{error_message}</grey>'))
print_formatted_text('')
continue
break
return value
def save_settings_confirmation() -> bool:
return (
cli_confirm(
'\nSave new settings? (They will take effect after restart)',
['Yes, save', 'No, discard'],
)
== 0
)
async def modify_llm_settings_basic(
config: AppConfig, settings_store: FileSettingsStore
):
model_list = get_supported_llm_models(config)
organized_models = organize_models_and_providers(model_list)
provider_list = list(organized_models.keys())
verified_providers = [p for p in VERIFIED_PROVIDERS if p in provider_list]
provider_list = [p for p in provider_list if p not in verified_providers]
provider_list = verified_providers + provider_list
provider_completer = FuzzyWordCompleter(provider_list)
session = PromptSession(key_bindings=kb_cancel())
provider = None
model = None
api_key = None
try:
provider = await get_validated_input(
session,
'(Step 1/3) Select LLM Provider (TAB for options, CTRL-c to cancel): ',
completer=provider_completer,
validator=lambda x: x in organized_models,
error_message='Invalid provider selected',
)
model_list = organized_models[provider]['models']
if provider == 'openai':
model_list = [m for m in model_list if m not in VERIFIED_OPENAI_MODELS]
model_list = VERIFIED_OPENAI_MODELS + model_list
if provider == 'anthropic':
model_list = [m for m in model_list if m not in VERIFIED_ANTHROPIC_MODELS]
model_list = VERIFIED_ANTHROPIC_MODELS + model_list
model_completer = FuzzyWordCompleter(model_list)
model = await get_validated_input(
session,
'(Step 2/3) Select LLM Model (TAB for options, CTRL-c to cancel): ',
completer=model_completer,
validator=lambda x: x in organized_models[provider]['models'],
error_message=f'Invalid model selected for provider {provider}',
)
api_key = await get_validated_input(
session,
'(Step 3/3) Enter API Key (CTRL-c to cancel): ',
error_message='API Key cannot be empty',
)
except (
UserCancelledError,
KeyboardInterrupt,
EOFError,
):
return # Return on exception
# TODO: check for empty string inputs?
# Handle case where a prompt might return None unexpectedly
if provider is None or model is None or api_key is None:
return
save_settings = save_settings_confirmation()
if not save_settings:
return
llm_config = config.get_llm_config()
llm_config.model = provider + organized_models[provider]['separator'] + model
llm_config.api_key = SecretStr(api_key)
llm_config.base_url = None
config.set_llm_config(llm_config)
config.default_agent = OH_DEFAULT_AGENT
config.security.confirmation_mode = False
config.enable_default_condenser = True
agent_config = config.get_agent_config(config.default_agent)
agent_config.condenser = LLMSummarizingCondenserConfig(
llm_config=llm_config,
type='llm',
)
config.set_agent_config(agent_config, config.default_agent)
settings = await settings_store.load()
if not settings:
settings = Settings()
settings.llm_model = provider + organized_models[provider]['separator'] + model
settings.llm_api_key = SecretStr(api_key)
settings.llm_base_url = None
settings.agent = OH_DEFAULT_AGENT
settings.confirmation_mode = False
settings.enable_default_condenser = True
await settings_store.store(settings)
async def modify_llm_settings_advanced(
config: AppConfig, settings_store: FileSettingsStore
):
session = PromptSession(key_bindings=kb_cancel())
custom_model = None
base_url = None
api_key = None
agent = None
try:
custom_model = await get_validated_input(
session,
'(Step 1/6) Custom Model (CTRL-c to cancel): ',
error_message='Custom Model cannot be empty',
)
base_url = await get_validated_input(
session,
'(Step 2/6) Base URL (CTRL-c to cancel): ',
error_message='Base URL cannot be empty',
)
api_key = await get_validated_input(
session,
'(Step 3/6) API Key (CTRL-c to cancel): ',
error_message='API Key cannot be empty',
)
agent_list = Agent.list_agents()
agent_completer = FuzzyWordCompleter(agent_list)
agent = await get_validated_input(
session,
'(Step 4/6) Agent (TAB for options, CTRL-c to cancel): ',
completer=agent_completer,
validator=lambda x: x in agent_list,
error_message='Invalid agent selected',
)
enable_confirmation_mode = (
cli_confirm(
question='(Step 5/6) Confirmation Mode (CTRL-c to cancel):',
choices=['Enable', 'Disable'],
)
== 0
)
enable_memory_condensation = (
cli_confirm(
question='(Step 6/6) Memory Condensation (CTRL-c to cancel):',
choices=['Enable', 'Disable'],
)
== 0
)
except (
UserCancelledError,
KeyboardInterrupt,
EOFError,
):
return # Return on exception
# TODO: check for empty string inputs?
# Handle case where a prompt might return None unexpectedly
if custom_model is None or base_url is None or api_key is None or agent is None:
return
save_settings = save_settings_confirmation()
if not save_settings:
return
llm_config = config.get_llm_config()
llm_config.model = custom_model
llm_config.base_url = base_url
llm_config.api_key = SecretStr(api_key)
config.set_llm_config(llm_config)
config.default_agent = agent
config.security.confirmation_mode = enable_confirmation_mode
agent_config = config.get_agent_config(config.default_agent)
if enable_memory_condensation:
agent_config.condenser = LLMSummarizingCondenserConfig(
llm_config=llm_config,
type='llm',
)
else:
agent_config.condenser = NoOpCondenserConfig(type='noop')
config.set_agent_config(agent_config)
settings = await settings_store.load()
if not settings:
settings = Settings()
settings.llm_model = custom_model
settings.llm_api_key = SecretStr(api_key)
settings.llm_base_url = base_url
settings.agent = agent
settings.confirmation_mode = enable_confirmation_mode
settings.enable_default_condenser = enable_memory_condensation
await settings_store.store(settings)
+580
View File
@@ -0,0 +1,580 @@
# CLI TUI input and output functions
# Handles all input and output to the console
# CLI Settings are handled separately in cli_settings.py
import asyncio
import sys
import time
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.application import Application
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.formatted_text import HTML, FormattedText, StyleAndTextTuples
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.shortcuts import print_container
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
from openhands import __version__
from openhands.core.config import AppConfig
from openhands.events import EventSource
from openhands.events.action import (
Action,
ActionConfirmationStatus,
CmdRunAction,
FileEditAction,
MessageAction,
)
from openhands.events.event import Event
from openhands.events.observation import (
CmdOutputObservation,
FileEditObservation,
FileReadObservation,
)
from openhands.llm.metrics import Metrics
# Color and styling constants
COLOR_GOLD = '#FFD700'
COLOR_GREY = '#808080'
DEFAULT_STYLE = Style.from_dict(
{
'gold': COLOR_GOLD,
'grey': COLOR_GREY,
'prompt': f'{COLOR_GOLD} bold',
}
)
COMMANDS = {
'/exit': 'Exit the application',
'/help': 'Display available commands',
'/init': 'Initialize a new repository',
'/status': 'Display session details and usage metrics',
'/new': 'Create a new session',
'/settings': 'Display and modify current settings',
}
class UsageMetrics:
def __init__(self):
self.metrics: Metrics = Metrics()
self.session_init_time: float = time.time()
class CustomDiffLexer(Lexer):
"""Custom lexer for the specific diff format."""
def lex_document(self, document) -> StyleAndTextTuples:
lines = document.lines
def get_line(lineno: int) -> StyleAndTextTuples:
line = lines[lineno]
if line.startswith('+'):
return [('ansigreen', line)]
elif line.startswith('-'):
return [('ansired', line)]
elif line.startswith('[') or line.startswith('('):
# Style for metadata lines like [Existing file...] or (content...)
return [('bold', line)]
else:
# Default style for other lines
return [('', line)]
return get_line
# CLI initialization and startup display functions
def display_runtime_initialization_message(runtime: str):
print_formatted_text('')
if runtime == 'local':
print_formatted_text(HTML('<grey>⚙️ Starting local runtime...</grey>'))
elif runtime == 'docker':
print_formatted_text(HTML('<grey>🐳 Starting Docker runtime...</grey>'))
print_formatted_text('')
def display_initialization_animation(text, is_loaded: asyncio.Event):
ANIMATION_FRAMES = ['', '', '', '', '', '', '', '', '', '']
i = 0
while not is_loaded.is_set():
sys.stdout.write('\n')
sys.stdout.write(
f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
)
sys.stdout.flush()
time.sleep(0.1)
i += 1
sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
sys.stdout.flush()
def display_banner(session_id: str, is_loaded: asyncio.Event):
print_formatted_text(
HTML(r"""<gold>
___ _ _ _
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|_|
</gold>"""),
style=DEFAULT_STYLE,
)
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
banner_text = (
'Initialized session' if is_loaded.is_set() else 'Initializing session'
)
print_formatted_text('')
print_formatted_text(HTML(f'<grey>{banner_text} {session_id}</grey>'))
print_formatted_text('')
def display_welcome_message():
print_formatted_text(
HTML("<gold>Let's start building!</gold>\n"), style=DEFAULT_STYLE
)
print_formatted_text(
HTML('What do you want to build? <grey>Type /help for help</grey>'),
style=DEFAULT_STYLE,
)
def display_initial_user_prompt(prompt: str):
print_formatted_text(
FormattedText(
[
('', '\n'),
(COLOR_GOLD, '> '),
('', prompt),
]
)
)
# Prompt output display functions
def display_event(event: Event, config: AppConfig) -> None:
if isinstance(event, Action):
if hasattr(event, 'thought'):
display_message(event.thought)
if isinstance(event, MessageAction):
if event.source == EventSource.AGENT:
display_message(event.content)
if isinstance(event, CmdRunAction):
display_command(event)
if isinstance(event, CmdOutputObservation):
display_command_output(event.content)
if isinstance(event, FileEditAction):
display_file_edit(event)
if isinstance(event, FileEditObservation):
display_file_edit(event)
if isinstance(event, FileReadObservation):
display_file_read(event)
def display_message(message: str):
time.sleep(0.2)
message = message.strip()
if message:
print_formatted_text(f'\n{message}')
def display_command(event: CmdRunAction):
if event.confirmation_state == ActionConfirmationStatus.AWAITING_CONFIRMATION:
container = Frame(
TextArea(
text=f'$ {event.command}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Action',
style='ansired',
)
print_formatted_text('')
print_container(container)
def display_command_output(output: str):
lines = output.split('\n')
formatted_lines = []
for line in lines:
if line.startswith('[Python Interpreter') or line.startswith('openhands@'):
# TODO: clean this up once we clean up terminal output
continue
formatted_lines.append(line)
formatted_lines.append('\n')
# Remove the last newline if it exists
if formatted_lines:
formatted_lines.pop()
container = Frame(
TextArea(
text=''.join(formatted_lines),
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Action Output',
style=f'fg:{COLOR_GREY}',
)
print_formatted_text('')
print_container(container)
def display_file_edit(event: FileEditAction | FileEditObservation):
if isinstance(event, FileEditObservation):
container = Frame(
TextArea(
text=event.visualize_diff(n_context_lines=4),
read_only=True,
wrap_lines=True,
lexer=CustomDiffLexer(),
),
title='File Edit',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
def display_file_read(event: FileReadObservation):
container = Frame(
TextArea(
text=f'{event}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='File Read',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
# Interactive command output display functions
def display_help():
# Version header and introduction
print_formatted_text(
HTML(
f'\n<grey>OpenHands CLI v{__version__}</grey>\n'
'<gold>OpenHands CLI lets you interact with the OpenHands agent from the command line.</gold>\n'
)
)
# Usage examples
print_formatted_text('Things that you can try:')
print_formatted_text(
HTML(
'• Ask questions about the codebase <grey>> How does main.py work?</grey>\n'
'• Edit files or add new features <grey>> Add a new function to ...</grey>\n'
'• Find and fix issues <grey>> Fix the type error in ...</grey>\n'
)
)
# Tips section
print_formatted_text(
'Some tips to get the most out of OpenHands:\n'
'• Be as specific as possible about the desired outcome or the problem to be solved.\n'
'• Provide context, including relevant file paths and line numbers if available.\n'
'• Break large tasks into smaller, manageable prompts.\n'
'• Include relevant error messages or logs.\n'
'• Specify the programming language or framework, if not obvious.\n'
)
# Commands section
print_formatted_text(HTML('Interactive commands:'))
commands_html = ''
for command, description in COMMANDS.items():
commands_html += f'<gold><b>{command}</b></gold> - <grey>{description}</grey>\n'
print_formatted_text(HTML(commands_html))
# Footer
print_formatted_text(
HTML(
'<grey>Learn more at: https://docs.all-hands.dev/modules/usage/getting-started</grey>'
)
)
def display_usage_metrics(usage_metrics: UsageMetrics):
cost_str = f'${usage_metrics.metrics.accumulated_cost:.6f}'
input_tokens_str = (
f'{usage_metrics.metrics.accumulated_token_usage.prompt_tokens:,}'
)
cache_read_str = (
f'{usage_metrics.metrics.accumulated_token_usage.cache_read_tokens:,}'
)
cache_write_str = (
f'{usage_metrics.metrics.accumulated_token_usage.cache_write_tokens:,}'
)
output_tokens_str = (
f'{usage_metrics.metrics.accumulated_token_usage.completion_tokens:,}'
)
total_tokens_str = f'{usage_metrics.metrics.accumulated_token_usage.prompt_tokens + usage_metrics.metrics.accumulated_token_usage.completion_tokens:,}'
labels_and_values = [
(' Total Cost (USD):', cost_str),
('', ''),
(' Total Input Tokens:', input_tokens_str),
(' Cache Hits:', cache_read_str),
(' Cache Writes:', cache_write_str),
(' Total Output Tokens:', output_tokens_str),
('', ''),
(' Total Tokens:', total_tokens_str),
]
# Calculate max widths for alignment
max_label_width = max(len(label) for label, _ in labels_and_values)
max_value_width = max(len(value) for _, value in labels_and_values)
# Construct the summary text with aligned columns
summary_lines = [
f'{label:<{max_label_width}} {value:<{max_value_width}}'
for label, value in labels_and_values
]
summary_text = '\n'.join(summary_lines)
container = Frame(
TextArea(
text=summary_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Usage Metrics',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
def get_session_duration(session_init_time: float) -> str:
current_time = time.time()
session_duration = current_time - session_init_time
hours, remainder = divmod(session_duration, 3600)
minutes, seconds = divmod(remainder, 60)
return f'{int(hours)}h {int(minutes)}m {int(seconds)}s'
def display_shutdown_message(usage_metrics: UsageMetrics, session_id: str):
duration_str = get_session_duration(usage_metrics.session_init_time)
print_formatted_text(HTML('<grey>Closing current session...</grey>'))
print_formatted_text('')
display_usage_metrics(usage_metrics)
print_formatted_text('')
print_formatted_text(HTML(f'<grey>Session duration: {duration_str}</grey>'))
print_formatted_text('')
print_formatted_text(HTML(f'<grey>Closed session {session_id}</grey>'))
print_formatted_text('')
def display_status(usage_metrics: UsageMetrics, session_id: str):
duration_str = get_session_duration(usage_metrics.session_init_time)
print_formatted_text('')
print_formatted_text(HTML(f'<grey>Session ID: {session_id}</grey>'))
print_formatted_text(HTML(f'<grey>Uptime: {duration_str}</grey>'))
print_formatted_text('')
display_usage_metrics(usage_metrics)
# Common input functions
class CommandCompleter(Completer):
"""Custom completer for commands."""
def get_completions(self, document, complete_event):
text = document.text
# Only show completions if the user has typed '/'
if text.startswith('/'):
# If just '/' is typed, show all commands
if text == '/':
for command, description in COMMANDS.items():
yield Completion(
command[1:], # Remove the leading '/' as it's already typed
start_position=0,
display=f'{command} - {description}',
)
# Otherwise show matching commands
else:
for command, description in COMMANDS.items():
if command.startswith(text):
yield Completion(
command[len(text) :], # Complete the remaining part
start_position=0,
display=f'{command} - {description}',
)
prompt_session = PromptSession(style=DEFAULT_STYLE)
# RPrompt animation related variables
SPINNER_FRAMES = [
'[ ■□□□ ]',
'[ □■□□ ]',
'[ □□■□ ]',
'[ □□□■ ]',
'[ □□■□ ]',
'[ □■□□ ]',
]
ANIMATION_INTERVAL = 0.2 # seconds
current_frame_index = 0
last_update_time = time.monotonic()
# RPrompt function for the user confirmation
def get_rprompt() -> FormattedText:
"""
Returns the current animation frame for the rprompt.
This function is called by prompt_toolkit during rendering.
"""
global current_frame_index, last_update_time
# Only update the frame if enough time has passed
# This prevents excessive recalculation during rendering
now = time.monotonic()
if now - last_update_time > ANIMATION_INTERVAL:
current_frame_index = (current_frame_index + 1) % len(SPINNER_FRAMES)
last_update_time = now
# Return the frame wrapped in FormattedText
return FormattedText(
[
('', ' '), # Add a space before the spinner
(COLOR_GOLD, SPINNER_FRAMES[current_frame_index]),
]
)
async def read_prompt_input(multiline=False):
try:
if multiline:
kb = KeyBindings()
@kb.add('c-d')
def _(event):
event.current_buffer.validate_and_handle()
with patch_stdout():
print_formatted_text('')
message = await prompt_session.prompt_async(
'Enter your message and press Ctrl+D to finish:\n',
multiline=True,
key_bindings=kb,
)
else:
with patch_stdout():
print_formatted_text('')
prompt_session.completer = CommandCompleter()
message = await prompt_session.prompt_async(
'> ',
)
return message
except (KeyboardInterrupt, EOFError):
return '/exit'
async def read_confirmation_input():
try:
with patch_stdout():
prompt_session.completer = None
confirmation = await prompt_session.prompt_async(
'Proceed with action? (y)es/(n)o > ',
rprompt=get_rprompt,
refresh_interval=ANIMATION_INTERVAL / 2,
)
prompt_session.rprompt = None
confirmation = confirmation.strip().lower()
return confirmation in ['y', 'yes']
except (KeyboardInterrupt, EOFError):
return False
def cli_confirm(
question: str = 'Are you sure?', choices: list[str] | None = None
) -> int:
"""
Display a confirmation prompt with the given question and choices.
Returns the index of the selected choice.
"""
if choices is None:
choices = ['Yes', 'No']
selected = [0] # Using list to allow modification in closure
def get_choice_text():
return [
('class:question', f'{question}\n\n'),
] + [
(
'class:selected' if i == selected[0] else 'class:unselected',
f"{'> ' if i == selected[0] else ' '}{choice}\n",
)
for i, choice in enumerate(choices)
]
kb = KeyBindings()
@kb.add('up')
def _(event):
selected[0] = (selected[0] - 1) % len(choices)
@kb.add('down')
def _(event):
selected[0] = (selected[0] + 1) % len(choices)
@kb.add('enter')
def _(event):
event.app.exit(result=selected[0])
style = Style.from_dict({'selected': COLOR_GOLD, 'unselected': ''})
layout = Layout(
HSplit(
[
Window(
FormattedTextControl(get_choice_text),
always_hide_cursor=True,
)
]
)
)
app = Application(
layout=layout,
key_bindings=kb,
style=style,
mouse_support=True,
full_screen=False,
)
return app.run(in_thread=True)
def kb_cancel():
"""Custom key bindings to handle ESC as a user cancellation."""
bindings = KeyBindings()
@bindings.add('escape')
def _(event):
event.app.exit(exception=UserCancelledError, style='class:aborting')
return bindings
class UserCancelledError(Exception):
"""Raised when the user cancels an operation via key binding."""
pass
+152
View File
@@ -0,0 +1,152 @@
from pathlib import Path
from typing import Dict, List
import toml
from openhands.core.cli_tui import (
UsageMetrics,
)
from openhands.events.event import Event
from openhands.llm.metrics import Metrics
_LOCAL_CONFIG_FILE_PATH = Path.home() / '.openhands' / 'config.toml'
_DEFAULT_CONFIG: Dict[str, Dict[str, List[str]]] = {'sandbox': {'trusted_dirs': []}}
def get_local_config_trusted_dirs() -> list[str]:
if _LOCAL_CONFIG_FILE_PATH.exists():
with open(_LOCAL_CONFIG_FILE_PATH, 'r') as f:
try:
config = toml.load(f)
except Exception:
config = _DEFAULT_CONFIG
if 'sandbox' in config and 'trusted_dirs' in config['sandbox']:
return config['sandbox']['trusted_dirs']
return []
def add_local_config_trusted_dir(folder_path: str):
config = _DEFAULT_CONFIG
if _LOCAL_CONFIG_FILE_PATH.exists():
try:
with open(_LOCAL_CONFIG_FILE_PATH, 'r') as f:
config = toml.load(f)
except Exception:
config = _DEFAULT_CONFIG
else:
_LOCAL_CONFIG_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
if 'sandbox' not in config:
config['sandbox'] = {}
if 'trusted_dirs' not in config['sandbox']:
config['sandbox']['trusted_dirs'] = []
if folder_path not in config['sandbox']['trusted_dirs']:
config['sandbox']['trusted_dirs'].append(folder_path)
with open(_LOCAL_CONFIG_FILE_PATH, 'w') as f:
toml.dump(config, f)
def update_usage_metrics(event: Event, usage_metrics: UsageMetrics):
if not hasattr(event, 'llm_metrics'):
return
llm_metrics: Metrics | None = event.llm_metrics
if not llm_metrics:
return
usage_metrics.metrics = llm_metrics
def extract_model_and_provider(model):
separator = '/'
split = model.split(separator)
if len(split) == 1:
# no "/" separator found, try with "."
separator = '.'
split = model.split(separator)
if split_is_actually_version(split):
split = [separator.join(split)] # undo the split
if len(split) == 1:
# no "/" or "." separator found
if split[0] in VERIFIED_OPENAI_MODELS:
return {'provider': 'openai', 'model': split[0], 'separator': '/'}
if split[0] in VERIFIED_ANTHROPIC_MODELS:
return {'provider': 'anthropic', 'model': split[0], 'separator': '/'}
# return as model only
return {'provider': '', 'model': model, 'separator': ''}
provider = split[0]
model_id = separator.join(split[1:])
return {'provider': provider, 'model': model_id, 'separator': separator}
def organize_models_and_providers(models):
result = {}
for model in models:
extracted = extract_model_and_provider(model)
separator = extracted['separator']
provider = extracted['provider']
model_id = extracted['model']
# Ignore "anthropic" providers with a separator of "."
# These are outdated and incompatible providers.
if provider == 'anthropic' and separator == '.':
continue
key = provider or 'other'
if key not in result:
result[key] = {'separator': separator, 'models': []}
result[key]['models'].append(model_id)
return result
VERIFIED_PROVIDERS = ['openai', 'azure', 'anthropic', 'deepseek']
VERIFIED_OPENAI_MODELS = [
'gpt-4o',
'gpt-4o-mini',
'gpt-4-turbo',
'gpt-4',
'gpt-4-32k',
'o1-mini',
'o1',
'o3-mini',
'o3-mini-2025-01-31',
]
VERIFIED_ANTHROPIC_MODELS = [
'claude-2',
'claude-2.1',
'claude-3-5-sonnet-20240620',
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-haiku-20240307',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-7-sonnet-20250219',
]
def is_number(char):
return char.isdigit()
def split_is_actually_version(split):
return len(split) > 1 and split[1] and split[1][0] and is_number(split[1][0])
def read_file(file_path):
with open(file_path, 'r') as f:
return f.read()
def write_to_file(file_path, content):
with open(file_path, 'w') as f:
f.write(content)
+2
View File
@@ -38,6 +38,7 @@ class SandboxConfig(BaseModel):
enable_gpu: Whether to enable GPU.
docker_runtime_kwargs: Additional keyword arguments to pass to the Docker runtime when running containers.
This should be a JSON string that will be parsed into a dictionary.
trusted_dirs: List of directories that can be trusted to run the OpenHands CLI.
"""
remote_runtime_api_url: str | None = Field(default='http://localhost:8000')
@@ -75,6 +76,7 @@ class SandboxConfig(BaseModel):
enable_gpu: bool = Field(default=False)
docker_runtime_kwargs: dict | None = Field(default=None)
selected_repo: str | None = Field(default=None)
trusted_dirs: list[str] = Field(default_factory=list)
model_config = {'extra': 'forbid'}
+2 -3
View File
@@ -100,9 +100,8 @@ def initialize_repository_for_runtime(
The repository directory path if a repository was cloned, None otherwise.
"""
# clone selected repository if provided
github_token = (
SecretStr(os.environ.get('GITHUB_TOKEN')) if not github_token else github_token
)
if github_token is None and 'GITHUB_TOKEN' in os.environ:
github_token = SecretStr(os.environ['GITHUB_TOKEN'])
secret_store = (
SecretStore(
+1 -3
View File
@@ -16,11 +16,9 @@ class CmdRunAction(Action):
)
is_input: bool = False # if True, the command is an input to the running process
thought: str = ''
blocking: bool = False
blocking: bool = False # if True, the command will be run in a blocking manner, but a timeout must be set through _set_hard_timeout
is_static: bool = False # if True, runs the command in a separate process
cwd: str | None = None # current working directory, only used if is_static is True
# If blocking is True, the command will be run in a blocking manner.
# e.g., it will NOT return early due to soft timeout.
hidden: bool = False
action: str = ActionType.RUN
runnable: ClassVar[bool] = True
-1
View File
@@ -5,7 +5,6 @@ from typing import Any
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.events import Event, EventSource
from openhands.events.serialization.action import action_from_dict
from openhands.events.serialization.observation import observation_from_dict
@@ -1,5 +1,6 @@
import json
import os
from datetime import datetime
from typing import Any
import httpx
@@ -18,8 +19,6 @@ from openhands.integrations.service_types import (
)
from openhands.server.types import AppMode
from openhands.utils.import_utils import get_impl
from datetime import datetime
class GitHubService(BaseGitService, GitService):
@@ -159,12 +158,9 @@ class GitHubService(BaseGitService, GitService):
return repos[:max_repos] # Trim to max_repos if needed
def parse_pushed_at_date(self, repo):
ts = repo.get("pushed_at")
return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ") if ts else datetime.min
ts = repo.get('pushed_at')
return datetime.strptime(ts, '%Y-%m-%dT%H:%M:%SZ') if ts else datetime.min
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
MAX_REPOS = 1000
@@ -192,11 +188,9 @@ class GitHubService(BaseGitService, GitService):
# If we've already reached MAX_REPOS, no need to check other installations
if len(all_repos) >= MAX_REPOS:
break
if sort == "pushed":
all_repos.sort(
key=self.parse_pushed_at_date, reverse=True
)
if sort == 'pushed':
all_repos.sort(key=self.parse_pushed_at_date, reverse=True)
else:
# Original behavior for non-SaaS mode
params = {'per_page': str(PER_PAGE), 'sort': sort}
@@ -205,7 +199,6 @@ class GitHubService(BaseGitService, GitService):
# Fetch user repositories
all_repos = await self._fetch_paginated_repos(url, params, MAX_REPOS)
# Convert to Repository objects
return [
Repository(
+18 -14
View File
@@ -108,7 +108,9 @@ class GitLabService(BaseGitService, GitService):
except httpx.HTTPError as e:
raise self.handle_http_error(e)
async def execute_graphql_query(self, query: str, variables: dict[str, Any] = {}) -> Any:
async def execute_graphql_query(
self, query: str, variables: dict[str, Any] | None = None
) -> Any:
"""
Execute a GraphQL query against the GitLab GraphQL API
@@ -119,6 +121,8 @@ class GitLabService(BaseGitService, GitService):
Returns:
The data portion of the GraphQL response
"""
if variables is None:
variables = {}
try:
async with httpx.AsyncClient() as client:
gitlab_headers = await self._get_gitlab_headers()
@@ -294,7 +298,7 @@ class GitLabService(BaseGitService, GitService):
try:
tasks: list[SuggestedTask] = []
# Get merge requests using GraphQL
response = await self.execute_graphql_query(query)
data = response.get('currentUser', {})
@@ -343,27 +347,27 @@ class GitLabService(BaseGitService, GitService):
title=title,
)
)
# Get assigned issues using REST API
url = f"{self.BASE_URL}/issues"
url = f'{self.BASE_URL}/issues'
params = {
"assignee_username": username,
"state": "opened",
"scope": "assigned_to_me"
'assignee_username': username,
'state': 'opened',
'scope': 'assigned_to_me',
}
issues_response, _ = await self._make_request(
method=RequestMethod.GET,
url=url,
params=params
method=RequestMethod.GET, url=url, params=params
)
# Process issues
for issue in issues_response:
repo_name = issue.get('references', {}).get('full', '').split('#')[0].strip()
repo_name = (
issue.get('references', {}).get('full', '').split('#')[0].strip()
)
issue_number = issue.get('iid')
title = issue.get('title', '')
tasks.append(
SuggestedTask(
git_provider=ProviderType.GITLAB,

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