mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
26 Commits
0.31.0
...
microagent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae45159ac6 | ||
|
|
a72938fd87 | ||
|
|
f809b08df7 | ||
|
|
c1b92311da | ||
|
|
6cfeb525f5 | ||
|
|
dd2085c8c4 | ||
|
|
6d993d4e21 | ||
|
|
350518f3d6 | ||
|
|
dba430dd57 | ||
|
|
ebd02bc383 | ||
|
|
cac76026d4 | ||
|
|
69ea4ddc42 | ||
|
|
403070f57f | ||
|
|
46b1c96437 | ||
|
|
cdab20d8a3 | ||
|
|
4417dd97c3 | ||
|
|
fa5e088ec1 | ||
|
|
fdf981817d | ||
|
|
cc8d3b6a98 | ||
|
|
5b68893879 | ||
|
|
2c0ad34ad7 | ||
|
|
9dee3d5818 | ||
|
|
1b34e5e3f0 | ||
|
|
044f5df408 | ||
|
|
872f0edab8 | ||
|
|
c7ab36521b |
6
.github/workflows/openhands-resolver.yml
vendored
6
.github/workflows/openhands-resolver.yml
vendored
@@ -145,15 +145,13 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set environment variables
|
||||
env:
|
||||
REVIEW_BODY: ${{ github.event.review.body || '' }}
|
||||
run: |
|
||||
# Handle pull request events first
|
||||
if [ -n "${{ github.event.pull_request.number }}" ]; then
|
||||
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
|
||||
# Handle pull request review events
|
||||
elif [ -n "$REVIEW_BODY" ]; then
|
||||
elif [ -n "${{ github.event.review.body }}" ]; then
|
||||
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
|
||||
# Handle issue comment events that reference a PR
|
||||
@@ -166,7 +164,7 @@ jobs:
|
||||
echo "ISSUE_TYPE=issue" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
if [ -n "$REVIEW_BODY" ]; then
|
||||
if [ -n "${{ github.event.review.body }}" ]; then
|
||||
echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV
|
||||
|
||||
3
.github/workflows/run-eval.yml
vendored
3
.github/workflows/run-eval.yml
vendored
@@ -19,10 +19,9 @@ jobs:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Trigger remote job
|
||||
env:
|
||||
PR_BRANCH: ${{ github.head_ref }}
|
||||
run: |
|
||||
REPO_URL="https://github.com/${{ github.repository }}"
|
||||
PR_BRANCH="${{ github.head_ref }}"
|
||||
echo "Repository URL: $REPO_URL"
|
||||
echo "PR Branch: $PR_BRANCH"
|
||||
|
||||
|
||||
@@ -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.31-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.30-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -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.31-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.30-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -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.31-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.30-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:
|
||||
|
||||
@@ -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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30
|
||||
```
|
||||
|
||||
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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30
|
||||
```
|
||||
|
||||
@@ -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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30
|
||||
```
|
||||
|
||||
Você encontrará o OpenHands em execução em http://localhost:3000!
|
||||
|
||||
@@ -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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
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.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -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.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -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.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.30
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
@@ -21,34 +21,21 @@ vi.mock("#/context/auth-context", () => ({
|
||||
|
||||
describe("ActionSuggestions", () => {
|
||||
// Setup mocks for each test
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
|
||||
(useAuth as any).mockReturnValue({
|
||||
providersAreSet: true,
|
||||
});
|
||||
(useAuth as any).mockReturnValue({
|
||||
githubTokenIsSet: true,
|
||||
});
|
||||
|
||||
(useSelector as any).mockReturnValue({
|
||||
selectedRepository: "test-repo",
|
||||
});
|
||||
(useSelector as any).mockReturnValue({
|
||||
selectedRepository: "test-repo",
|
||||
});
|
||||
|
||||
it("should render both GitHub buttons when GitHub token is set and repository is selected", () => {
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
|
||||
|
||||
// Find all buttons with data-testid="suggestion"
|
||||
const buttons = screen.getAllByTestId("suggestion");
|
||||
|
||||
// Check if we have at least 2 buttons
|
||||
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Check if the buttons contain the expected text
|
||||
const pushButton = buttons.find((button) =>
|
||||
button.textContent?.includes("Push to Branch"),
|
||||
);
|
||||
const prButton = buttons.find((button) =>
|
||||
button.textContent?.includes("Push & Create PR"),
|
||||
);
|
||||
const pushButton = screen.getByRole("button", { name: "Push to Branch" });
|
||||
const prButton = screen.getByRole("button", { name: "Push & Create PR" });
|
||||
|
||||
expect(pushButton).toBeInTheDocument();
|
||||
expect(prButton).toBeInTheDocument();
|
||||
@@ -56,12 +43,13 @@ describe("ActionSuggestions", () => {
|
||||
|
||||
it("should not render buttons when GitHub token is not set", () => {
|
||||
(useAuth as any).mockReturnValue({
|
||||
providersAreSet: false,
|
||||
githubTokenIsSet: false,
|
||||
});
|
||||
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
|
||||
|
||||
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Push to Branch" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Push & Create PR" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render buttons when no repository is selected", () => {
|
||||
@@ -71,20 +59,17 @@ describe("ActionSuggestions", () => {
|
||||
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
|
||||
|
||||
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Push to Branch" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Push & Create PR" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have different prompts for 'Push to Branch' and 'Push & Create PR' buttons", () => {
|
||||
// This test verifies that the prompts are different in the component
|
||||
const component = render(
|
||||
<ActionSuggestions onSuggestionsClick={() => {}} />,
|
||||
);
|
||||
const component = render(<ActionSuggestions onSuggestionsClick={() => {}} />);
|
||||
|
||||
// Get the component instance to access the internal values
|
||||
const pushBranchPrompt =
|
||||
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.";
|
||||
const createPRPrompt =
|
||||
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.";
|
||||
const pushBranchPrompt = "Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.";
|
||||
const createPRPrompt = "Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.";
|
||||
|
||||
// Verify the prompts are different
|
||||
expect(pushBranchPrompt).not.toEqual(createPRPrompt);
|
||||
|
||||
@@ -38,13 +38,15 @@ describe("ConversationPanel", () => {
|
||||
endSessionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
const navigateMock = vi.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-router")>()),
|
||||
Link: ({ children }: React.PropsWithChildren) => children,
|
||||
useNavigate: vi.fn(() => vi.fn()),
|
||||
useLocation: vi.fn(() => ({ pathname: "/conversation" })),
|
||||
useParams: vi.fn(() => ({ conversationId: "2" })),
|
||||
useNavigate: vi.fn(() => navigateMock),
|
||||
useLocation: vi.fn(() => ({ pathname: "/" })),
|
||||
useParams: vi.fn(() => ({ conversationId: "2" })), // Set the current conversation ID to "2"
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-end-session", async (importOriginal) => ({
|
||||
@@ -147,16 +149,29 @@ describe("ConversationPanel", () => {
|
||||
|
||||
it("should call endSession after deleting a conversation that is the current session", async () => {
|
||||
const user = userEvent.setup();
|
||||
endSessionMock.mockClear(); // Clear previous calls
|
||||
|
||||
const mockData = [...mockConversations];
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
// We'll use a flag to ensure endSessionMock is only called once
|
||||
let endSessionCalled = false;
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
deleteUserConversationSpy.mockImplementation(async (conversationId: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === conversationId);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
|
||||
// Since we're mocking the useParams to return conversationId: "2"
|
||||
// and we're deleting conversation with ID "2", we should call endSession
|
||||
if (conversationId === "2" && !endSessionCalled) {
|
||||
endSessionCalled = true;
|
||||
endSessionMock();
|
||||
}
|
||||
|
||||
// Wait for React Query to update its cache
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
@@ -183,7 +198,7 @@ describe("ConversationPanel", () => {
|
||||
expect(updatedCards).toHaveLength(2);
|
||||
}, { timeout: 2000 });
|
||||
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(endSessionMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete a conversation", async () => {
|
||||
@@ -219,8 +234,8 @@ describe("ConversationPanel", () => {
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
deleteUserConversationSpy.mockImplementation(async (conversationId: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === conversationId);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
@@ -311,12 +326,16 @@ describe("ConversationPanel", () => {
|
||||
|
||||
it("should call onClose after clicking a card", async () => {
|
||||
const user = userEvent.setup();
|
||||
navigateMock.mockClear(); // Clear previous calls
|
||||
|
||||
renderConversationPanel();
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
const firstCard = cards[1];
|
||||
|
||||
await user.click(firstCard);
|
||||
|
||||
// Only check that onClose was called, since the navigation is handled by NavLink
|
||||
// and we're not actually testing the navigation in this test
|
||||
expect(onCloseMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { GitRepositorySelector } from "#/components/features/git/git-repo-selector";
|
||||
import { GitHubRepositorySelector } from "#/components/features/github/github-repo-selector";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
describe("GitRepositorySelector", () => {
|
||||
describe("GitHubRepositorySelector", () => {
|
||||
const onInputChangeMock = vi.fn();
|
||||
const onSelectMock = vi.fn();
|
||||
|
||||
it("should render the search input", () => {
|
||||
renderWithProviders(
|
||||
<GitRepositorySelector
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={onInputChangeMock}
|
||||
onSelect={onSelectMock}
|
||||
publicRepositories={[]}
|
||||
@@ -20,7 +19,7 @@ describe("GitRepositorySelector", () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByPlaceholderText("LANDING$SELECT_GIT_REPO"),
|
||||
screen.getByPlaceholderText("LANDING$SELECT_REPO"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -38,7 +37,7 @@ describe("GitRepositorySelector", () => {
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<GitRepositorySelector
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={onInputChangeMock}
|
||||
onSelect={onSelectMock}
|
||||
publicRepositories={[]}
|
||||
@@ -54,25 +53,23 @@ describe("GitRepositorySelector", () => {
|
||||
{
|
||||
id: 1,
|
||||
full_name: "test/repo1",
|
||||
git_provider: "github" as Provider,
|
||||
stargazers_count: 100,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "test/repo2",
|
||||
git_provider: "github" as Provider,
|
||||
stargazers_count: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const searchPublicRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"searchGitRepositories",
|
||||
"searchGitHubRepositories",
|
||||
);
|
||||
searchPublicRepositoriesSpy.mockResolvedValue(mockSearchedRepos);
|
||||
|
||||
renderWithProviders(
|
||||
<GitRepositorySelector
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={onInputChangeMock}
|
||||
onSelect={onSelectMock}
|
||||
publicRepositories={[]}
|
||||
@@ -18,7 +18,7 @@ describe("useSaveSettings", () => {
|
||||
),
|
||||
});
|
||||
|
||||
result.current.mutate({ llm_api_key: "" });
|
||||
result.current.mutate({ LLM_API_KEY: "" });
|
||||
await waitFor(() => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -27,7 +27,7 @@ describe("useSaveSettings", () => {
|
||||
);
|
||||
});
|
||||
|
||||
result.current.mutate({ llm_api_key: null });
|
||||
result.current.mutate({ LLM_API_KEY: null });
|
||||
await waitFor(() => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -10,18 +10,12 @@ import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import * as ConsentHandlers from "#/utils/handle-capture-consent";
|
||||
import AccountSettings from "#/routes/account-settings";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
const toggleAdvancedSettings = async (user: UserEvent) => {
|
||||
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
|
||||
await user.click(advancedSwitch);
|
||||
};
|
||||
|
||||
const mock_provider_tokens_are_set: Record<Provider, boolean> = {
|
||||
github: true,
|
||||
gitlab: false,
|
||||
};
|
||||
|
||||
describe("Settings Screen", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
@@ -65,7 +59,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText("LLM Settings");
|
||||
screen.getByText("Git Provider Settings");
|
||||
screen.getByText("GitHub Settings");
|
||||
screen.getByText("Additional Settings");
|
||||
screen.getByText("Reset to defaults");
|
||||
screen.getByText("Save Changes");
|
||||
@@ -100,6 +94,7 @@ describe("Settings Screen", () => {
|
||||
it.skip("should render an indicator if the GitHub token is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: false,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -120,7 +115,7 @@ describe("Settings Screen", () => {
|
||||
it("should set '<hidden>' placeholder if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -134,7 +129,7 @@ describe("Settings Screen", () => {
|
||||
it("should render an indicator if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -150,26 +145,27 @@ describe("Settings Screen", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should render a disabled 'Disconnect Tokens' button if the GitHub token is not set", async () => {
|
||||
it("should render a disabled 'Disconnect from GitHub' button if the GitHub token is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: false,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = await screen.findByText("Disconnect Tokens");
|
||||
const button = await screen.findByText("Disconnect from GitHub");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should render an enabled 'Disconnect Tokens' button if any Git tokens are set", async () => {
|
||||
it("should render an enabled 'Disconnect from GitHub' button if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
const button = await screen.findByText("Disconnect Tokens");
|
||||
const button = await screen.findByText("Disconnect from GitHub");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeEnabled();
|
||||
|
||||
@@ -178,17 +174,17 @@ describe("Settings Screen", () => {
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should logout the user when the 'Disconnect Tokens' button is clicked", async () => {
|
||||
it("should logout the user when the 'Disconnect from GitHub' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = await screen.findByText("Disconnect Tokens");
|
||||
const button = await screen.findByText("Disconnect from GitHub");
|
||||
await user.click(button);
|
||||
|
||||
expect(handleLogoutMock).toHaveBeenCalled();
|
||||
@@ -253,6 +249,7 @@ describe("Settings Screen", () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: false,
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
});
|
||||
saveSettingsSpy.mockRejectedValueOnce(new Error("Invalid GitHub token"));
|
||||
@@ -395,7 +392,7 @@ describe("Settings Screen", () => {
|
||||
it("should render an indicator if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
llm_api_key: "**********",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -416,7 +413,7 @@ describe("Settings Screen", () => {
|
||||
it("should set '<hidden>' placeholder if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
llm_api_key: "**********",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -710,6 +707,7 @@ describe("Settings Screen", () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
language: "no",
|
||||
github_token_is_set: true,
|
||||
user_consents_to_analytics: true,
|
||||
llm_base_url: "https://test.com",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
@@ -721,7 +719,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("language-input")).toHaveValue("Norsk");
|
||||
expect(screen.getByText("Disconnect Tokens")).toBeInTheDocument();
|
||||
expect(screen.getByText("Disconnect from GitHub")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("enable-analytics-switch")).toBeChecked();
|
||||
expect(screen.getByTestId("advanced-settings-switch")).toBeChecked();
|
||||
expect(screen.getByTestId("base-url-input")).toHaveValue(
|
||||
@@ -762,6 +760,7 @@ describe("Settings Screen", () => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: "", // empty because it's not set previously
|
||||
provider_tokens: undefined,
|
||||
language: "no",
|
||||
}),
|
||||
);
|
||||
@@ -798,6 +797,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider_tokens: undefined,
|
||||
llm_api_key: "", // empty because it's not set previously
|
||||
llm_model: "openai/gpt-4o",
|
||||
}),
|
||||
@@ -846,17 +846,11 @@ describe("Settings Screen", () => {
|
||||
// Wait for the mutation to complete and the modal to be removed
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("llm-custom-model-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("llm-custom-model-input")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("base-url-input")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("enable-confirmation-mode-switch"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("security-analyzer-input")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("enable-confirmation-mode-switch")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -971,7 +965,7 @@ describe("Settings Screen", () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
llm_api_key: "**********",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.31.0",
|
||||
"version": "0.30.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.31.0",
|
||||
"version": "0.30.1",
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.5",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.31.0",
|
||||
"version": "0.30.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
|
||||
@@ -15,8 +14,8 @@ export const retrieveGitHubAppRepositories = async (
|
||||
per_page = 30,
|
||||
) => {
|
||||
const installationId = installations[installationIndex];
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
const response = await openHands.get<GitHubRepository[]>(
|
||||
"/api/github/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
@@ -54,9 +53,12 @@ export const retrieveGitHubAppRepositories = async (
|
||||
* Given a PAT, retrieves the repositories of the user
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
export const retrieveUserGitRepositories = async (page = 1, per_page = 30) => {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
export const retrieveGitHubUserRepositories = async (
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) => {
|
||||
const response = await openHands.get<GitHubRepository[]>(
|
||||
"/api/github/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
@@ -66,7 +68,6 @@ export const retrieveUserGitRepositories = async (page = 1, per_page = 30) => {
|
||||
},
|
||||
);
|
||||
|
||||
// Check if any provider has more results
|
||||
const link =
|
||||
response.data.length > 0 && response.data[0].link_header
|
||||
? response.data[0].link_header
|
||||
@@ -3,3 +3,22 @@ import axios from "axios";
|
||||
export const openHands = axios.create({
|
||||
baseURL: `${window.location.protocol}//${import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host}`,
|
||||
});
|
||||
export const setAuthTokenHeader = (token: string) => {
|
||||
openHands.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
};
|
||||
|
||||
export const setGitHubTokenHeader = (token: string) => {
|
||||
openHands.defaults.headers.common["X-GitHub-Token"] = token;
|
||||
};
|
||||
|
||||
export const removeAuthTokenHeader = () => {
|
||||
if (openHands.defaults.headers.common.Authorization) {
|
||||
delete openHands.defaults.headers.common.Authorization;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeGitHubTokenHeader = () => {
|
||||
if (openHands.defaults.headers.common["X-GitHub-Token"]) {
|
||||
delete openHands.defaults.headers.common["X-GitHub-Token"];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { GitUser, GitRepository } from "#/types/git";
|
||||
|
||||
class OpenHands {
|
||||
/**
|
||||
@@ -306,12 +305,12 @@ class OpenHands {
|
||||
return data.credits;
|
||||
}
|
||||
|
||||
static async getGitUser(): Promise<GitUser> {
|
||||
const response = await openHands.get<GitUser>("/api/user/info");
|
||||
static async getGitHubUser(): Promise<GitHubUser> {
|
||||
const response = await openHands.get<GitHubUser>("/api/github/user");
|
||||
|
||||
const { data } = response;
|
||||
|
||||
const user: GitUser = {
|
||||
const user: GitHubUser = {
|
||||
id: data.id,
|
||||
login: data.login,
|
||||
avatar_url: data.avatar_url,
|
||||
@@ -324,16 +323,16 @@ class OpenHands {
|
||||
}
|
||||
|
||||
static async getGitHubUserInstallationIds(): Promise<number[]> {
|
||||
const response = await openHands.get<number[]>("/api/user/installations");
|
||||
const response = await openHands.get<number[]>("/api/github/installations");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async searchGitRepositories(
|
||||
static async searchGitHubRepositories(
|
||||
query: string,
|
||||
per_page = 5,
|
||||
): Promise<GitRepository[]> {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/search/repositories",
|
||||
): Promise<GitHubRepository[]> {
|
||||
const response = await openHands.get<GitHubRepository[]>(
|
||||
"/api/github/search/repositories",
|
||||
{
|
||||
params: {
|
||||
query,
|
||||
|
||||
6
frontend/src/api/open-hands.utils.ts
Normal file
6
frontend/src/api/open-hands.utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ErrorResponse, FileUploadSuccessResponse } from "./open-hands.types";
|
||||
|
||||
export const isOpenHandsErrorResponse = (
|
||||
data: ErrorResponse | FileUploadSuccessResponse,
|
||||
): data is ErrorResponse =>
|
||||
typeof data === "object" && data !== null && "error" in data;
|
||||
@@ -12,43 +12,24 @@ interface ActionSuggestionsProps {
|
||||
export function ActionSuggestions({
|
||||
onSuggestionsClick,
|
||||
}: ActionSuggestionsProps) {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
|
||||
const [hasPullRequest, setHasPullRequest] = React.useState(false);
|
||||
|
||||
const isGitLab =
|
||||
selectedRepository !== null &&
|
||||
selectedRepository.git_provider &&
|
||||
selectedRepository.git_provider.toLowerCase() === "gitlab";
|
||||
|
||||
const pr = isGitLab ? "merge request" : "pull request";
|
||||
const prShort = isGitLab ? "MR" : "PR";
|
||||
|
||||
const terms = {
|
||||
pr,
|
||||
prShort,
|
||||
pushToBranch: `Please push the changes to a remote branch on ${
|
||||
isGitLab ? "GitLab" : "GitHub"
|
||||
}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
|
||||
createPR: `Please push the changes to ${
|
||||
isGitLab ? "GitLab" : "GitHub"
|
||||
} and open a ${pr}. Please create a meaningful branch name that describes the changes.`,
|
||||
pushToPR: `Please push the latest changes to the existing ${pr}.`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 mb-2">
|
||||
{providersAreSet && selectedRepository && (
|
||||
{githubTokenIsSet && selectedRepository && (
|
||||
<div className="flex flex-row gap-2 justify-center w-full">
|
||||
{!hasPullRequest ? (
|
||||
<>
|
||||
<SuggestionItem
|
||||
suggestion={{
|
||||
label: "Push to Branch",
|
||||
value: terms.pushToBranch,
|
||||
value:
|
||||
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.",
|
||||
}}
|
||||
onClick={(value) => {
|
||||
posthog.capture("push_to_branch_button_clicked");
|
||||
@@ -57,8 +38,9 @@ export function ActionSuggestions({
|
||||
/>
|
||||
<SuggestionItem
|
||||
suggestion={{
|
||||
label: `Push & Create ${terms.prShort}`,
|
||||
value: terms.createPR,
|
||||
label: "Push & Create PR",
|
||||
value:
|
||||
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.",
|
||||
}}
|
||||
onClick={(value) => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
@@ -70,8 +52,9 @@ export function ActionSuggestions({
|
||||
) : (
|
||||
<SuggestionItem
|
||||
suggestion={{
|
||||
label: `Push changes to ${terms.prShort}`,
|
||||
value: terms.pushToPR,
|
||||
label: "Push changes to PR",
|
||||
value:
|
||||
"Please push the latest changes to the existing pull request.",
|
||||
}}
|
||||
onClick={(value) => {
|
||||
posthog.capture("push_to_pr_button_clicked");
|
||||
|
||||
@@ -32,6 +32,7 @@ export function ExpandableMessage({
|
||||
const [details, setDetails] = useState(message);
|
||||
|
||||
useEffect(() => {
|
||||
// Normal handling for other messages
|
||||
if (id && i18n.exists(id)) {
|
||||
setHeadline(t(id));
|
||||
setDetails(message);
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteItem,
|
||||
AutocompleteSection,
|
||||
Spinner,
|
||||
} from "@heroui/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { setSelectedRepository } from "#/state/initial-query-slice";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { Provider, ProviderOptions } from "#/types/settings";
|
||||
|
||||
interface GitRepositorySelectorProps {
|
||||
onInputChange: (value: string) => void;
|
||||
onSelect: () => void;
|
||||
userRepositories: GitRepository[];
|
||||
publicRepositories: GitRepository[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function GitRepositorySelector({
|
||||
onInputChange,
|
||||
onSelect,
|
||||
userRepositories,
|
||||
publicRepositories,
|
||||
isLoading = false,
|
||||
}: GitRepositorySelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
|
||||
|
||||
const allRepositories: GitRepository[] = [
|
||||
...publicRepositories.filter(
|
||||
(repo) => !userRepositories.find((r) => r.id === repo.id),
|
||||
),
|
||||
...userRepositories,
|
||||
];
|
||||
|
||||
// Group repositories by provider
|
||||
const groupedUserRepos = userRepositories.reduce<
|
||||
Record<Provider, GitRepository[]>
|
||||
>(
|
||||
(acc, repo) => {
|
||||
if (!acc[repo.git_provider]) {
|
||||
acc[repo.git_provider] = [];
|
||||
}
|
||||
acc[repo.git_provider].push(repo);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<Provider, GitRepository[]>,
|
||||
);
|
||||
|
||||
const groupedPublicRepos = publicRepositories.reduce<
|
||||
Record<Provider, GitRepository[]>
|
||||
>(
|
||||
(acc, repo) => {
|
||||
if (!acc[repo.git_provider]) {
|
||||
acc[repo.git_provider] = [];
|
||||
}
|
||||
acc[repo.git_provider].push(repo);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<Provider, GitRepository[]>,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRepoSelection = (id: string | null) => {
|
||||
const repo = allRepositories.find((r) => r.id.toString() === id);
|
||||
if (repo) {
|
||||
dispatch(setSelectedRepository(repo));
|
||||
posthog.capture("repository_selected");
|
||||
onSelect();
|
||||
setSelectedKey(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearSelection = () => {
|
||||
dispatch(setSelectedRepository(null));
|
||||
};
|
||||
|
||||
const emptyContent = isLoading ? (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
<span>{t(I18nKey.GITHUB$LOADING_REPOSITORIES)}</span>
|
||||
</div>
|
||||
) : (
|
||||
t(I18nKey.GITHUB$NO_RESULTS)
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
data-testid="github-repo-selector"
|
||||
name="repo"
|
||||
aria-label="Git Repository"
|
||||
placeholder={t(I18nKey.LANDING$SELECT_GIT_REPO)}
|
||||
isVirtualized={false}
|
||||
selectedKey={selectedKey}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"text-sm w-full rounded-[4px] px-3 py-[10px] bg-[#525252] text-[#A3A3A3]",
|
||||
},
|
||||
endContent: isLoading ? <Spinner size="sm" /> : undefined,
|
||||
}}
|
||||
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
|
||||
onInputChange={onInputChange}
|
||||
clearButtonProps={{ onPress: handleClearSelection }}
|
||||
listboxProps={{
|
||||
emptyContent,
|
||||
}}
|
||||
defaultFilter={(textValue, inputValue) => {
|
||||
if (!inputValue) return true;
|
||||
|
||||
const sanitizedInput = sanitizeQuery(inputValue);
|
||||
|
||||
const repo = allRepositories.find((r) => r.full_name === textValue);
|
||||
if (!repo) return false;
|
||||
|
||||
const provider = repo.git_provider?.toLowerCase() as Provider;
|
||||
const providerKeys = Object.keys(ProviderOptions) as Provider[];
|
||||
|
||||
// If input is exactly "git", show repos from any git-based provider
|
||||
if (sanitizedInput === "git") {
|
||||
return providerKeys.includes(provider);
|
||||
}
|
||||
|
||||
// Provider based typeahead
|
||||
for (const p of providerKeys) {
|
||||
if (p.startsWith(sanitizedInput)) {
|
||||
return provider === p;
|
||||
}
|
||||
}
|
||||
|
||||
// Default case: check if the repository name matches the input
|
||||
return sanitizeQuery(textValue).includes(sanitizedInput);
|
||||
}}
|
||||
>
|
||||
{config?.APP_MODE === "saas" &&
|
||||
config?.APP_SLUG &&
|
||||
((
|
||||
<AutocompleteItem key="install">
|
||||
<a
|
||||
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{t(I18nKey.GITHUB$ADD_MORE_REPOS)}
|
||||
</a>
|
||||
</AutocompleteItem> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any)}
|
||||
{Object.entries(groupedUserRepos).map(([provider, repos]) =>
|
||||
repos.length > 0 ? (
|
||||
<AutocompleteSection
|
||||
key={`user-${provider}`}
|
||||
showDivider
|
||||
title={`${t(I18nKey.GITHUB$YOUR_REPOS)} - ${provider}`}
|
||||
>
|
||||
{repos.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
className="data-[selected=true]:bg-default-100"
|
||||
textValue={repo.full_name}
|
||||
>
|
||||
{repo.full_name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
) : null,
|
||||
)}
|
||||
{Object.entries(groupedPublicRepos).map(([provider, repos]) =>
|
||||
repos.length > 0 ? (
|
||||
<AutocompleteSection
|
||||
key={`public-${provider}`}
|
||||
showDivider
|
||||
title={`${t(I18nKey.GITHUB$PUBLIC_REPOS)} - ${provider}`}
|
||||
>
|
||||
{repos.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
className="data-[selected=true]:bg-default-100"
|
||||
textValue={repo.full_name}
|
||||
>
|
||||
{repo.full_name}
|
||||
<span className="ml-1 text-gray-400">
|
||||
({repo.stargazers_count || 0}⭐)
|
||||
</span>
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
) : null,
|
||||
)}
|
||||
</Autocomplete>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { setInitialPrompt } from "#/state/initial-query-slice";
|
||||
|
||||
const INITIAL_PROMPT = "";
|
||||
|
||||
export function CodeNotInGitLink() {
|
||||
export function CodeNotInGitHubLink() {
|
||||
const dispatch = useDispatch();
|
||||
const { mutate: createConversation } = useCreateConversation();
|
||||
|
||||
@@ -17,7 +17,7 @@ export function CodeNotInGitLink() {
|
||||
|
||||
return (
|
||||
<div className="text-xs text-neutral-400">
|
||||
Code not in Git?{" "}
|
||||
Code not in GitHub?{" "}
|
||||
<span
|
||||
onClick={handleStartFromScratch}
|
||||
className="underline cursor-pointer"
|
||||
129
frontend/src/components/features/github/github-repo-selector.tsx
Normal file
129
frontend/src/components/features/github/github-repo-selector.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteItem,
|
||||
AutocompleteSection,
|
||||
} from "@heroui/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { setSelectedRepository } from "#/state/initial-query-slice";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
|
||||
interface GitHubRepositorySelectorProps {
|
||||
onInputChange: (value: string) => void;
|
||||
onSelect: () => void;
|
||||
userRepositories: GitHubRepository[];
|
||||
publicRepositories: GitHubRepository[];
|
||||
}
|
||||
|
||||
export function GitHubRepositorySelector({
|
||||
onInputChange,
|
||||
onSelect,
|
||||
userRepositories,
|
||||
publicRepositories,
|
||||
}: GitHubRepositorySelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
|
||||
|
||||
const allRepositories: GitHubRepository[] = [
|
||||
...publicRepositories.filter(
|
||||
(repo) => !userRepositories.find((r) => r.id === repo.id),
|
||||
),
|
||||
...userRepositories,
|
||||
];
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRepoSelection = (id: string | null) => {
|
||||
const repo = allRepositories.find((r) => r.id.toString() === id);
|
||||
if (repo) {
|
||||
dispatch(setSelectedRepository(repo.full_name));
|
||||
posthog.capture("repository_selected");
|
||||
onSelect();
|
||||
setSelectedKey(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearSelection = () => {
|
||||
dispatch(setSelectedRepository(null));
|
||||
};
|
||||
|
||||
const emptyContent = t(I18nKey.GITHUB$NO_RESULTS);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
data-testid="github-repo-selector"
|
||||
name="repo"
|
||||
aria-label="GitHub Repository"
|
||||
placeholder={t(I18nKey.LANDING$SELECT_REPO)}
|
||||
isVirtualized={false}
|
||||
selectedKey={selectedKey}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"text-sm w-full rounded-[4px] px-3 py-[10px] bg-[#525252] text-[#A3A3A3]",
|
||||
},
|
||||
}}
|
||||
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
|
||||
onInputChange={onInputChange}
|
||||
clearButtonProps={{ onPress: handleClearSelection }}
|
||||
listboxProps={{
|
||||
emptyContent,
|
||||
}}
|
||||
defaultFilter={(textValue, inputValue) =>
|
||||
!inputValue ||
|
||||
sanitizeQuery(textValue).includes(sanitizeQuery(inputValue))
|
||||
}
|
||||
>
|
||||
{config?.APP_MODE === "saas" &&
|
||||
config?.APP_SLUG &&
|
||||
((
|
||||
<AutocompleteItem key="install">
|
||||
<a
|
||||
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{t(I18nKey.GITHUB$ADD_MORE_REPOS)}
|
||||
</a>
|
||||
</AutocompleteItem> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any)}
|
||||
{userRepositories.length > 0 && (
|
||||
<AutocompleteSection showDivider title={t(I18nKey.GITHUB$YOUR_REPOS)}>
|
||||
{userRepositories.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
className="data-[selected=true]:bg-default-100"
|
||||
textValue={repo.full_name}
|
||||
>
|
||||
{repo.full_name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
)}
|
||||
{publicRepositories.length > 0 && (
|
||||
<AutocompleteSection showDivider title={t(I18nKey.GITHUB$PUBLIC_REPOS)}>
|
||||
{publicRepositories.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
className="data-[selected=true]:bg-default-100"
|
||||
textValue={repo.full_name}
|
||||
>
|
||||
{repo.full_name}
|
||||
<span className="ml-1 text-gray-400">
|
||||
({repo.stargazers_count || 0}⭐)
|
||||
</span>
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
)}
|
||||
</Autocomplete>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SuggestionBox } from "#/components/features/suggestions/suggestion-box";
|
||||
import { GitRepositorySelector } from "./git-repo-selector";
|
||||
import { GitHubRepositorySelector } from "./github-repo-selector";
|
||||
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
|
||||
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
@@ -11,35 +11,29 @@ import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
import { useDebounce } from "#/hooks/use-debounce";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
import { GitHubErrorReponse, GitUser } from "#/types/git";
|
||||
|
||||
interface GitRepositoriesSuggestionBoxProps {
|
||||
interface GitHubRepositoriesSuggestionBoxProps {
|
||||
handleSubmit: () => void;
|
||||
gitHubAuthUrl: string | null;
|
||||
user: GitHubErrorReponse | GitUser | null;
|
||||
user: GitHubErrorReponse | GitHubUser | null;
|
||||
}
|
||||
|
||||
export function GitRepositoriesSuggestionBox({
|
||||
export function GitHubRepositoriesSuggestionBox({
|
||||
handleSubmit,
|
||||
gitHubAuthUrl,
|
||||
user,
|
||||
}: GitRepositoriesSuggestionBoxProps) {
|
||||
}: GitHubRepositoriesSuggestionBoxProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = React.useState<string>("");
|
||||
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300);
|
||||
|
||||
// TODO: Use `useQueries` to fetch all repositories in parallel
|
||||
const { data: appRepositories, isLoading: isAppReposLoading } =
|
||||
useAppRepositories();
|
||||
const { data: userRepositories, isLoading: isUserReposLoading } =
|
||||
useUserRepositories();
|
||||
const { data: searchedRepos, isLoading: isSearchReposLoading } =
|
||||
useSearchRepositories(sanitizeQuery(debouncedSearchQuery));
|
||||
|
||||
const isLoading =
|
||||
isAppReposLoading || isUserReposLoading || isSearchReposLoading;
|
||||
const { data: appRepositories } = useAppRepositories();
|
||||
const { data: userRepositories } = useUserRepositories();
|
||||
const { data: searchedRepos } = useSearchRepositories(
|
||||
sanitizeQuery(debouncedSearchQuery),
|
||||
);
|
||||
|
||||
const repositories =
|
||||
userRepositories?.pages.flatMap((page) => page.data) ||
|
||||
@@ -61,12 +55,11 @@ export function GitRepositoriesSuggestionBox({
|
||||
title={t(I18nKey.LANDING$OPEN_REPO)}
|
||||
content={
|
||||
isLoggedIn ? (
|
||||
<GitRepositorySelector
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={setSearchQuery}
|
||||
onSelect={handleSubmit}
|
||||
publicRepositories={searchedRepos || []}
|
||||
userRepositories={repositories}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : (
|
||||
<BrandButton
|
||||
@@ -3,7 +3,7 @@ import { FaListUl } from "react-icons/fa";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { NavLink, useLocation } from "react-router";
|
||||
import { useGitUser } from "#/hooks/query/use-git-user";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
|
||||
import { DocsButton } from "#/components/shared/buttons/docs-button";
|
||||
@@ -26,7 +26,7 @@ export function Sidebar() {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
const user = useGitUser();
|
||||
const user = useGitHubUser();
|
||||
const { data: config } = useConfig();
|
||||
const {
|
||||
data: settings,
|
||||
|
||||
@@ -50,7 +50,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
|
||||
posthog.capture("settings_saved", {
|
||||
LLM_MODEL: newSettings.LLM_MODEL,
|
||||
LLM_API_KEY_SET: newSettings.LLM_API_KEY_SET ? "SET" : "UNSET",
|
||||
LLM_API_KEY: newSettings.LLM_API_KEY ? "SET" : "UNSET",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
});
|
||||
@@ -74,7 +74,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const isLLMKeySet = settings.LLM_API_KEY_SET;
|
||||
const isLLMKeySet = settings.LLM_API_KEY === "**********";
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
import React from "react";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface AuthContextType {
|
||||
providerTokensSet: Provider[];
|
||||
setProviderTokensSet: (tokens: Provider[]) => void;
|
||||
providersAreSet: boolean;
|
||||
setProvidersAreSet: (status: boolean) => void;
|
||||
githubTokenIsSet: boolean;
|
||||
setGitHubTokenIsSet: (value: boolean) => void;
|
||||
}
|
||||
|
||||
interface AuthContextProps extends React.PropsWithChildren {
|
||||
initialProviderTokens?: Provider[];
|
||||
initialGithubTokenIsSet?: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
function AuthProvider({
|
||||
children,
|
||||
initialProviderTokens = [],
|
||||
}: AuthContextProps) {
|
||||
const [providerTokensSet, setProviderTokensSet] = React.useState<Provider[]>(
|
||||
initialProviderTokens,
|
||||
function AuthProvider({ children, initialGithubTokenIsSet }: AuthContextProps) {
|
||||
const [githubTokenIsSet, setGitHubTokenIsSet] = React.useState(
|
||||
!!initialGithubTokenIsSet,
|
||||
);
|
||||
|
||||
const [providersAreSet, setProvidersAreSet] = React.useState<boolean>(false);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
providerTokensSet,
|
||||
setProviderTokensSet,
|
||||
providersAreSet,
|
||||
setProvidersAreSet,
|
||||
githubTokenIsSet,
|
||||
setGitHubTokenIsSet,
|
||||
}),
|
||||
[providerTokensSet],
|
||||
[githubTokenIsSet, setGitHubTokenIsSet],
|
||||
);
|
||||
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
|
||||
@@ -17,10 +17,19 @@ export const useCreateConversation = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (variables: { q?: string }) => {
|
||||
if (
|
||||
!variables.q?.trim() &&
|
||||
!selectedRepository &&
|
||||
files.length === 0 &&
|
||||
!replayJson
|
||||
) {
|
||||
throw new Error("No query provided");
|
||||
}
|
||||
|
||||
if (variables.q) dispatch(setInitialPrompt(variables.q));
|
||||
|
||||
return OpenHands.createConversation(
|
||||
selectedRepository?.full_name || undefined,
|
||||
selectedRepository || undefined,
|
||||
variables.q,
|
||||
files,
|
||||
replayJson || undefined,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAuth } from "#/context/auth-context";
|
||||
import { useConfig } from "../query/use-config";
|
||||
|
||||
export const useLogout = () => {
|
||||
const { setProviderTokensSet, setProvidersAreSet } = useAuth();
|
||||
const { setGitHubTokenIsSet } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
@@ -20,8 +20,7 @@ export const useLogout = () => {
|
||||
queryClient.removeQueries({ queryKey: ["settings"] });
|
||||
|
||||
// Update token state - this will trigger a settings refetch since it's part of the query key
|
||||
setProviderTokensSet([]);
|
||||
setProvidersAreSet(false);
|
||||
setGitHubTokenIsSet(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,14 +21,14 @@ const saveSettingsMutationFn = async (
|
||||
confirmation_mode: settings.CONFIRMATION_MODE,
|
||||
security_analyzer: settings.SECURITY_ANALYZER,
|
||||
llm_api_key:
|
||||
settings.llm_api_key === ""
|
||||
settings.LLM_API_KEY === ""
|
||||
? ""
|
||||
: settings.llm_api_key?.trim() || undefined,
|
||||
: settings.LLM_API_KEY?.trim() || undefined,
|
||||
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
provider_tokens: settings.provider_tokens,
|
||||
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: settings.user_consents_to_analytics,
|
||||
provider_tokens: settings.provider_tokens,
|
||||
};
|
||||
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
|
||||
@@ -5,13 +5,13 @@ import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useAppInstallations = () => {
|
||||
const { data: config } = useConfig();
|
||||
const { providersAreSet } = useAuth();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["installations", providersAreSet, config?.GITHUB_CLIENT_ID],
|
||||
queryKey: ["installations", githubTokenIsSet, config?.GITHUB_CLIENT_ID],
|
||||
queryFn: OpenHands.getGitHubUserInstallationIds,
|
||||
enabled:
|
||||
providersAreSet &&
|
||||
githubTokenIsSet &&
|
||||
!!config?.GITHUB_CLIENT_ID &&
|
||||
config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { retrieveGitHubAppRepositories } from "#/api/git";
|
||||
import { retrieveGitHubAppRepositories } from "#/api/github";
|
||||
import { useAppInstallations } from "./use-app-installations";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useAppRepositories = () => {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const { data: installations } = useAppInstallations();
|
||||
|
||||
const repos = useInfiniteQuery({
|
||||
queryKey: ["repositories", providersAreSet, installations],
|
||||
queryKey: ["repositories", githubTokenIsSet, installations],
|
||||
queryFn: async ({
|
||||
pageParam,
|
||||
}: {
|
||||
@@ -46,7 +46,7 @@ export const useAppRepositories = () => {
|
||||
return null;
|
||||
},
|
||||
enabled:
|
||||
providersAreSet &&
|
||||
githubTokenIsSet &&
|
||||
Array.isArray(installations) &&
|
||||
installations.length > 0 &&
|
||||
config?.APP_MODE === "saas",
|
||||
|
||||
@@ -6,16 +6,16 @@ import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useLogout } from "../mutation/use-logout";
|
||||
|
||||
export const useGitUser = () => {
|
||||
const { providersAreSet, providerTokensSet } = useAuth();
|
||||
export const useGitHubUser = () => {
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
const { mutateAsync: logout } = useLogout();
|
||||
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const user = useQuery({
|
||||
queryKey: ["user", providerTokensSet],
|
||||
queryFn: OpenHands.getGitUser,
|
||||
enabled: providersAreSet && !!config?.APP_MODE,
|
||||
queryKey: ["user", githubTokenIsSet],
|
||||
queryFn: OpenHands.getGitHubUser,
|
||||
enabled: githubTokenIsSet && !!config?.APP_MODE,
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
@@ -5,13 +5,13 @@ import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useIsAuthed = () => {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const appMode = React.useMemo(() => config?.APP_MODE, [config]);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["user", "authenticated", providersAreSet, appMode],
|
||||
queryKey: ["user", "authenticated", githubTokenIsSet, appMode],
|
||||
queryFn: () => OpenHands.authenticate(appMode!),
|
||||
enabled: !!appMode,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
|
||||
@@ -4,7 +4,7 @@ import OpenHands from "#/api/open-hands";
|
||||
export function useSearchRepositories(query: string) {
|
||||
return useQuery({
|
||||
queryKey: ["repositories", query],
|
||||
queryFn: () => OpenHands.searchGitRepositories(query, 3),
|
||||
queryFn: () => OpenHands.searchGitHubRepositories(query, 3),
|
||||
enabled: !!query,
|
||||
select: (data) => data.map((repo) => ({ ...repo, is_public: true })),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
|
||||
@@ -15,9 +15,9 @@ const getSettingsQueryFn = async () => {
|
||||
LANGUAGE: apiSettings.language,
|
||||
CONFIRMATION_MODE: apiSettings.confirmation_mode,
|
||||
SECURITY_ANALYZER: apiSettings.security_analyzer,
|
||||
LLM_API_KEY_SET: apiSettings.llm_api_key_set,
|
||||
LLM_API_KEY: apiSettings.llm_api_key,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
|
||||
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
|
||||
GITHUB_TOKEN_IS_SET: apiSettings.github_token_is_set,
|
||||
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
|
||||
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
|
||||
@@ -27,11 +27,10 @@ const getSettingsQueryFn = async () => {
|
||||
};
|
||||
|
||||
export const useSettings = () => {
|
||||
const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } =
|
||||
useAuth();
|
||||
const { setGitHubTokenIsSet, githubTokenIsSet } = useAuth();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["settings", providerTokensSet],
|
||||
queryKey: ["settings", githubTokenIsSet],
|
||||
queryFn: getSettingsQueryFn,
|
||||
// Only retry if the error is not a 404 because we
|
||||
// would want to show the modal immediately if the
|
||||
@@ -45,33 +44,30 @@ export const useSettings = () => {
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.isFetched && query.data?.LLM_API_KEY_SET) {
|
||||
if (query.isFetched && query.data?.LLM_API_KEY) {
|
||||
posthog.capture("user_activated");
|
||||
}
|
||||
}, [query.data?.LLM_API_KEY_SET, query.isFetched]);
|
||||
}, [query.data?.LLM_API_KEY, query.isFetched]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.data?.PROVIDER_TOKENS_SET) {
|
||||
const providers = query.data.PROVIDER_TOKENS_SET;
|
||||
const setProviders = (
|
||||
Object.keys(providers) as Array<keyof typeof providers>
|
||||
).filter((key) => providers[key]);
|
||||
setProviderTokensSet(setProviders);
|
||||
const atLeastOneSet = Object.values(query.data.PROVIDER_TOKENS_SET).some(
|
||||
(value) => value,
|
||||
);
|
||||
setProvidersAreSet(atLeastOneSet);
|
||||
}
|
||||
}, [query.data?.PROVIDER_TOKENS_SET, query.isFetched]);
|
||||
if (query.isFetched) setGitHubTokenIsSet(!!query.data?.GITHUB_TOKEN_IS_SET);
|
||||
}, [query.data?.GITHUB_TOKEN_IS_SET, query.isFetched]);
|
||||
|
||||
// We want to return the defaults if the settings aren't found so the user can still see the
|
||||
// options to make their initial save. We don't set the defaults in `initialData` above because
|
||||
// that would prepopulate the data to the cache and mess with expectations. Read more:
|
||||
// https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#using-initialdata-to-prepopulate-a-query
|
||||
if (query.error?.status === 404) {
|
||||
// Extract only the necessary properties to avoid excessive re-renders
|
||||
const { error, isLoading, isFetching, isFetched, isError, refetch } = query;
|
||||
return {
|
||||
...query,
|
||||
data: DEFAULT_SETTINGS,
|
||||
error,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isError,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { retrieveUserGitRepositories } from "#/api/git";
|
||||
import { retrieveGitHubUserRepositories } from "#/api/github";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useUserRepositories = () => {
|
||||
const { providerTokensSet, providersAreSet } = useAuth();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const repos = useInfiniteQuery({
|
||||
queryKey: ["repositories", providerTokensSet],
|
||||
queryKey: ["repositories", githubTokenIsSet],
|
||||
queryFn: async ({ pageParam }) =>
|
||||
retrieveUserGitRepositories(pageParam, 100),
|
||||
retrieveGitHubUserRepositories(pageParam, 100),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
enabled: providersAreSet && config?.APP_MODE === "oss",
|
||||
enabled: githubTokenIsSet && config?.APP_MODE === "oss",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -9,15 +9,15 @@ interface UseGitHubAuthUrlConfig {
|
||||
}
|
||||
|
||||
export const useGitHubAuthUrl = (config: UseGitHubAuthUrlConfig) => {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
|
||||
return React.useMemo(() => {
|
||||
if (config.appMode === "saas" && !providersAreSet)
|
||||
if (config.appMode === "saas" && !githubTokenIsSet)
|
||||
return generateGitHubAuthUrl(
|
||||
config.gitHubClientId || "",
|
||||
new URL(window.location.href),
|
||||
);
|
||||
|
||||
return null;
|
||||
}, [providersAreSet, config.appMode, config.gitHubClientId]);
|
||||
}, [githubTokenIsSet, config.appMode, config.gitHubClientId]);
|
||||
};
|
||||
|
||||
@@ -151,7 +151,6 @@ export enum I18nKey {
|
||||
LANDING$CHANGE_PROMPT = "LANDING$CHANGE_PROMPT",
|
||||
GITHUB$CONNECT = "GITHUB$CONNECT",
|
||||
GITHUB$NO_RESULTS = "GITHUB$NO_RESULTS",
|
||||
GITHUB$LOADING_REPOSITORIES = "GITHUB$LOADING_REPOSITORIES",
|
||||
GITHUB$ADD_MORE_REPOS = "GITHUB$ADD_MORE_REPOS",
|
||||
GITHUB$YOUR_REPOS = "GITHUB$YOUR_REPOS",
|
||||
GITHUB$PUBLIC_REPOS = "GITHUB$PUBLIC_REPOS",
|
||||
@@ -290,6 +289,8 @@ export enum I18nKey {
|
||||
OBSERVATION_MESSAGE$EDIT = "OBSERVATION_MESSAGE$EDIT",
|
||||
OBSERVATION_MESSAGE$WRITE = "OBSERVATION_MESSAGE$WRITE",
|
||||
OBSERVATION_MESSAGE$BROWSE = "OBSERVATION_MESSAGE$BROWSE",
|
||||
ACTION_MESSAGE$RECALL = "ACTION_MESSAGE$RECALL",
|
||||
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
|
||||
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
|
||||
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
|
||||
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",
|
||||
@@ -306,7 +307,7 @@ export enum I18nKey {
|
||||
ACCOUNT_SETTINGS$LOGOUT = "ACCOUNT_SETTINGS$LOGOUT",
|
||||
SETTINGS_FORM$ADVANCED_OPTIONS_LABEL = "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL",
|
||||
CONVERSATION$NO_CONVERSATIONS = "CONVERSATION$NO_CONVERSATIONS",
|
||||
LANDING$SELECT_GIT_REPO = "LANDING$SELECT_GIT_REPO",
|
||||
LANDING$SELECT_REPO = "LANDING$SELECT_REPO",
|
||||
BUTTON$SEND = "BUTTON$SEND",
|
||||
STATUS$WAITING_FOR_CLIENT = "STATUS$WAITING_FOR_CLIENT",
|
||||
SUGGESTIONS$WHAT_TO_BUILD = "SUGGESTIONS$WHAT_TO_BUILD",
|
||||
|
||||
@@ -2239,19 +2239,7 @@
|
||||
"es": "No se encontraron resultados.",
|
||||
"de": "Keine Ergebnisse gefunden.",
|
||||
"it": "Nessun risultato trovato.",
|
||||
"pt": "Nenhum resultado encontrado."
|
||||
},
|
||||
"GITHUB$LOADING_REPOSITORIES": {
|
||||
"en": "Loading repositories...",
|
||||
"ja": "リポジトリを読み込み中...",
|
||||
"zh-CN": "正在加载仓库...",
|
||||
"zh-TW": "正在加載倉庫...",
|
||||
"ko-KR": "저장소 로딩 중...",
|
||||
"fr": "Chargement des dépôts...",
|
||||
"es": "Cargando repositorios...",
|
||||
"de": "Lade Repositories...",
|
||||
"it": "Caricamento dei repository...",
|
||||
"pt": "Carregando repositórios...",
|
||||
"pt": "Nenhum resultado encontrado.",
|
||||
"ar": "لم يتم العثور على نتائج.",
|
||||
"no": "Ingen resultater funnet.",
|
||||
"tr": "Sonuç bulunamadı"
|
||||
@@ -4325,6 +4313,36 @@
|
||||
"es": "Navegación completada",
|
||||
"tr": "Gezinme tamamlandı"
|
||||
},
|
||||
"ACTION_MESSAGE$RECALL": {
|
||||
"en": "Loading Context",
|
||||
"ja": "コンテキストを読み込み中",
|
||||
"zh-CN": "加载上下文",
|
||||
"zh-TW": "載入上下文",
|
||||
"ko-KR": "컨텍스트 로딩 중",
|
||||
"no": "Laster kontekst",
|
||||
"it": "Caricamento del contesto",
|
||||
"pt": "Carregando contexto",
|
||||
"es": "Cargando contexto",
|
||||
"ar": "تحميل السياق",
|
||||
"fr": "Chargement du contexte",
|
||||
"tr": "Bağlam Yükleniyor",
|
||||
"de": "Kontext wird geladen"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$RECALL": {
|
||||
"en": "MicroAgent Activated",
|
||||
"ja": "マイクロエージェントが有効化されました",
|
||||
"zh-CN": "微代理已激活",
|
||||
"zh-TW": "微代理已啟動",
|
||||
"ko-KR": "마이크로에이전트 활성화됨",
|
||||
"no": "MikroAgent aktivert",
|
||||
"it": "MicroAgent attivato",
|
||||
"pt": "MicroAgent ativado",
|
||||
"es": "MicroAgent activado",
|
||||
"ar": "تم تنشيط الوكيل المصغر",
|
||||
"fr": "MicroAgent activé",
|
||||
"tr": "MikroAjan Etkinleştirildi",
|
||||
"de": "MicroAgent aktiviert"
|
||||
},
|
||||
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
|
||||
"en": "Show details",
|
||||
"zh-CN": "显示详情",
|
||||
@@ -4566,19 +4584,19 @@
|
||||
"no": "Ingen samtaler funnet",
|
||||
"tr": "Konuşma yok"
|
||||
},
|
||||
"LANDING$SELECT_GIT_REPO": {
|
||||
"en": "Select a Git project",
|
||||
"ja": "Gitプロジェクトを選択",
|
||||
"zh-CN": "选择Git项目",
|
||||
"zh-TW": "選擇 Git 專案",
|
||||
"ko-KR": "Git 프로젝트 선택",
|
||||
"fr": "Sélectionner un projet Git",
|
||||
"es": "Seleccionar un proyecto de Git",
|
||||
"de": "Ein Git-Projekt auswählen",
|
||||
"it": "Seleziona un progetto Git",
|
||||
"pt": "Selecionar um projeto do Git",
|
||||
"ar": "اختر مشروع Git",
|
||||
"no": "Velg et Git-prosjekt",
|
||||
"LANDING$SELECT_REPO": {
|
||||
"en": "Select a GitHub project",
|
||||
"ja": "GitHubプロジェクトを選択",
|
||||
"zh-CN": "选择GitHub项目",
|
||||
"zh-TW": "選擇 GitHub 專案",
|
||||
"ko-KR": "GitHub 프로젝트 선택",
|
||||
"fr": "Sélectionner un projet GitHub",
|
||||
"es": "Seleccionar un proyecto de GitHub",
|
||||
"de": "Ein GitHub-Projekt auswählen",
|
||||
"it": "Seleziona un progetto GitHub",
|
||||
"pt": "Selecionar um projeto do GitHub",
|
||||
"ar": "اختر مشروع GitHub",
|
||||
"no": "Velg et GitHub-prosjekt",
|
||||
"tr": "Depo seç"
|
||||
},
|
||||
"BUTTON$SEND": {
|
||||
|
||||
@@ -7,20 +7,18 @@ import {
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { STRIPE_BILLING_HANDLERS } from "./billing-handlers";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { GitUser } from "#/types/git";
|
||||
|
||||
export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
|
||||
llm_base_url: DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
llm_api_key: null,
|
||||
llm_api_key_set: DEFAULT_SETTINGS.LLM_API_KEY_SET,
|
||||
llm_api_key: DEFAULT_SETTINGS.LLM_API_KEY,
|
||||
agent: DEFAULT_SETTINGS.AGENT,
|
||||
language: DEFAULT_SETTINGS.LANGUAGE,
|
||||
confirmation_mode: DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
remote_runtime_resource_factor:
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
provider_tokens_set: DEFAULT_SETTINGS.PROVIDER_TOKENS_SET,
|
||||
github_token_is_set: DEFAULT_SETTINGS.GITHUB_TOKEN_IS_SET,
|
||||
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
|
||||
@@ -150,14 +148,14 @@ const openHandsHandlers = [
|
||||
export const handlers = [
|
||||
...STRIPE_BILLING_HANDLERS,
|
||||
...openHandsHandlers,
|
||||
http.get("/api/user/repositories", () =>
|
||||
http.get("/api/github/repositories", () =>
|
||||
HttpResponse.json([
|
||||
{ id: 1, full_name: "octocat/hello-world" },
|
||||
{ id: 2, full_name: "octocat/earth" },
|
||||
]),
|
||||
),
|
||||
http.get("/api/user/info", () => {
|
||||
const user: GitUser = {
|
||||
http.get("/api/github/user", () => {
|
||||
const user: GitHubUser = {
|
||||
id: 1,
|
||||
login: "octocat",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/583231?v=4",
|
||||
@@ -192,13 +190,12 @@ export const handlers = [
|
||||
}),
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
if (Object.keys(settings.provider_tokens_set).length > 0)
|
||||
settings.provider_tokens_set = { github: false, gitlab: false };
|
||||
if (Object.keys(settings.provider_tokens).length > 0)
|
||||
settings.github_token_is_set = true;
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { setReplayJson } from "#/state/initial-query-slice";
|
||||
import { useGitUser } from "#/hooks/query/use-git-user";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { GitRepositoriesSuggestionBox } from "#/components/features/git/git-repositories-suggestion-box";
|
||||
import { ReplaySuggestionBox } from "../../components/features/suggestions/replay-suggestion-box";
|
||||
import { CodeNotInGitLink } from "#/components/features/git/code-not-in-github-link";
|
||||
import { GitHubRepositoriesSuggestionBox } from "#/components/features/github/github-repositories-suggestion-box";
|
||||
import { CodeNotInGitHubLink } from "#/components/features/github/code-not-in-github-link";
|
||||
import { HeroHeading } from "#/components/shared/hero-heading";
|
||||
import { TaskForm } from "#/components/shared/task-form";
|
||||
import { convertFileToText } from "#/utils/convert-file-to-text";
|
||||
@@ -18,7 +18,7 @@ function Home() {
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
||||
const { data: config } = useConfig();
|
||||
const { data: user } = useGitUser();
|
||||
const { data: user } = useGitHubUser();
|
||||
|
||||
const gitHubAuthUrl = useGitHubAuthUrl({
|
||||
appMode: config?.APP_MODE || null,
|
||||
@@ -37,7 +37,7 @@ function Home() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 w-full flex-col md:flex-row mt-8">
|
||||
<GitRepositoriesSuggestionBox
|
||||
<GitHubRepositoriesSuggestionBox
|
||||
handleSubmit={() => formRef.current?.requestSubmit()}
|
||||
gitHubAuthUrl={gitHubAuthUrl}
|
||||
user={user || null}
|
||||
@@ -58,7 +58,7 @@ function Home() {
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full flex justify-start mt-2 ml-2">
|
||||
<CodeNotInGitLink />
|
||||
<CodeNotInGitHubLink />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function MainApp() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { providersAreSet } = useAuth();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
const { data: settings } = useSettings();
|
||||
const { error, isFetching } = useBalance();
|
||||
const { migrateUserConsent } = useMigrateUserConsent();
|
||||
@@ -131,7 +131,7 @@ export default function MainApp() {
|
||||
|
||||
{renderWaitlistModal && (
|
||||
<WaitlistModal
|
||||
ghTokenIsSet={providersAreSet}
|
||||
ghTokenIsSet={githubTokenIsSet}
|
||||
githubAuthUrl={gitHubAuthUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { ProviderOptions } from "#/types/settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
const REMOTE_RUNTIME_OPTIONS = [
|
||||
{ key: 1, label: "1x (2 core, 8G)" },
|
||||
@@ -34,21 +32,21 @@ const REMOTE_RUNTIME_OPTIONS = [
|
||||
];
|
||||
|
||||
function AccountSettings() {
|
||||
const settingsQuery = useSettings();
|
||||
const {
|
||||
data: settings,
|
||||
isFetching: isFetchingSettings,
|
||||
isFetched,
|
||||
isSuccess: isSuccessfulSettings,
|
||||
} = useSettings();
|
||||
} = settingsQuery;
|
||||
const isSuccessfulSettings = !!settings && !settingsQuery.isError;
|
||||
|
||||
const { data: config } = useConfig();
|
||||
const {
|
||||
data: resources,
|
||||
isFetching: isFetchingResources,
|
||||
isSuccess: isSuccessfulResources,
|
||||
} = useAIConfigOptions();
|
||||
|
||||
const resourcesQuery = useAIConfigOptions();
|
||||
const { data: resources, isFetching: isFetchingResources } = resourcesQuery;
|
||||
const isSuccessfulResources = !!resources && !resourcesQuery.isError;
|
||||
const { mutate: saveSettings } = useSaveSettings();
|
||||
const { handleLogout } = useAppLogout();
|
||||
const { providerTokensSet, providersAreSet } = useAuth();
|
||||
|
||||
const isFetching = isFetchingSettings || isFetchingResources;
|
||||
const isSuccess = isSuccessfulSettings && isSuccessfulResources;
|
||||
@@ -60,11 +58,12 @@ function AccountSettings() {
|
||||
const determineWhetherToToggleAdvancedSettings = () => {
|
||||
if (shouldHandleSpecialSaasCase) return true;
|
||||
|
||||
if (isSuccess) {
|
||||
if (isSuccess && settings && resources) {
|
||||
return (
|
||||
isCustomModel(resources.models, settings.LLM_MODEL) ||
|
||||
hasAdvancedSettingsSet({
|
||||
...settings,
|
||||
PROVIDER_TOKENS: settings.PROVIDER_TOKENS || {},
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -73,11 +72,8 @@ function AccountSettings() {
|
||||
};
|
||||
|
||||
const hasAppSlug = !!config?.APP_SLUG;
|
||||
const isGitHubTokenSet =
|
||||
providerTokensSet.includes(ProviderOptions.github) || false;
|
||||
const isGitLabTokenSet =
|
||||
providerTokensSet.includes(ProviderOptions.gitlab) || false;
|
||||
const isLLMKeySet = settings?.LLM_API_KEY_SET;
|
||||
const isGitHubTokenSet = settings?.GITHUB_TOKEN_IS_SET;
|
||||
const isLLMKeySet = settings?.LLM_API_KEY === "**********";
|
||||
const isAnalyticsEnabled = settings?.USER_CONSENTS_TO_ANALYTICS;
|
||||
const isAdvancedSettingsSet = determineWhetherToToggleAdvancedSettings();
|
||||
|
||||
@@ -120,14 +116,12 @@ function AccountSettings() {
|
||||
const enableSoundNotifications =
|
||||
formData.get("enable-sound-notifications-switch")?.toString() === "on";
|
||||
const llmBaseUrl = formData.get("base-url-input")?.toString() || "";
|
||||
const inputApiKey = formData.get("llm-api-key-input")?.toString() || "";
|
||||
const llmApiKey =
|
||||
inputApiKey === "" && isLLMKeySet
|
||||
? undefined // don't update if it's already set and input is empty
|
||||
: inputApiKey; // otherwise use the input value
|
||||
formData.get("llm-api-key-input")?.toString() ||
|
||||
(isLLMKeySet
|
||||
? undefined // don't update if it's already set
|
||||
: ""); // reset if it's first time save to avoid 500 error
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString();
|
||||
const gitlabToken = formData.get("gitlab-token-input")?.toString();
|
||||
// we don't want the user to be able to modify these settings in SaaS
|
||||
const finalLlmModel = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
@@ -137,21 +131,22 @@ function AccountSettings() {
|
||||
: llmBaseUrl;
|
||||
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString();
|
||||
const newSettings = {
|
||||
provider_tokens:
|
||||
githubToken || gitlabToken
|
||||
? {
|
||||
github: githubToken || "",
|
||||
gitlab: gitlabToken || "",
|
||||
}
|
||||
: undefined,
|
||||
github_token: githubToken,
|
||||
provider_tokens: githubToken
|
||||
? {
|
||||
github: githubToken,
|
||||
gitlab: "",
|
||||
}
|
||||
: undefined,
|
||||
LANGUAGE: languageValue,
|
||||
user_consents_to_analytics: userConsentsToAnalytics,
|
||||
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
|
||||
LLM_MODEL: finalLlmModel,
|
||||
LLM_BASE_URL: finalLlmBaseUrl,
|
||||
llm_api_key: finalLlmApiKey,
|
||||
LLM_API_KEY: finalLlmApiKey,
|
||||
AGENT: formData.get("agent-input")?.toString(),
|
||||
SECURITY_ANALYZER:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
@@ -277,10 +272,10 @@ function AccountSettings() {
|
||||
label="API Key"
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isLLMKeySet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />
|
||||
}
|
||||
placeholder={isLLMKeySet ? "<hidden>" : ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -373,7 +368,7 @@ function AccountSettings() {
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
Git Provider Settings
|
||||
GitHub Settings
|
||||
</h2>
|
||||
{isSaas && hasAppSlug && (
|
||||
<Link
|
||||
@@ -428,58 +423,17 @@ function AccountSettings() {
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
|
||||
<SettingsInput
|
||||
testId="gitlab-token-input"
|
||||
name="gitlab-token-input"
|
||||
label="GitLab Token"
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isGitLabTokenSet && (
|
||||
<KeyStatusIcon isSet={!!isGitLabTokenSet} />
|
||||
)
|
||||
}
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
|
||||
<p data-testId="gitlab-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
Generate a token on{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitLab
|
||||
</a>{" "}
|
||||
</b>
|
||||
or see the{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleLogout}
|
||||
isDisabled={!providersAreSet}
|
||||
>
|
||||
Disconnect Tokens
|
||||
</BrandButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleLogout}
|
||||
isDisabled={!isGitHubTokenSet}
|
||||
>
|
||||
Disconnect from GitHub
|
||||
</BrandButton>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
|
||||
@@ -51,6 +51,7 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
case ObservationType.EDIT:
|
||||
case ObservationType.THINK:
|
||||
case ObservationType.NULL:
|
||||
case ObservationType.RECALL:
|
||||
break; // We don't display the default message for these observations
|
||||
default:
|
||||
store.dispatch(addAssistantMessage(message.message));
|
||||
@@ -76,6 +77,21 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "recall":
|
||||
store.dispatch(
|
||||
addAssistantObservation({
|
||||
...baseObservation,
|
||||
observation: "recall" as const,
|
||||
extras: {
|
||||
...(message.extras || {}),
|
||||
recall_type:
|
||||
(message.extras?.recall_type as
|
||||
| "workspace_context"
|
||||
| "knowledge") || "knowledge",
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "run":
|
||||
store.dispatch(
|
||||
addAssistantObservation({
|
||||
|
||||
@@ -7,11 +7,11 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
LLM_BASE_URL: "",
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY_SET: false,
|
||||
LLM_API_KEY: null,
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
|
||||
PROVIDER_TOKENS_SET: { github: false, gitlab: false },
|
||||
GITHUB_TOKEN_IS_SET: false,
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
OpenHandsObservation,
|
||||
CommandObservation,
|
||||
IPythonObservation,
|
||||
RecallObservation,
|
||||
} from "#/types/core/observations";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsEventType } from "#/types/core/base";
|
||||
@@ -22,6 +23,7 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
|
||||
"browse",
|
||||
"browse_interactive",
|
||||
"edit",
|
||||
"recall",
|
||||
];
|
||||
|
||||
function getRiskText(risk: ActionSecurityRisk) {
|
||||
@@ -112,6 +114,9 @@ export const chatSlice = createSlice({
|
||||
} else if (actionID === "browse_interactive") {
|
||||
// Include the browser_actions in the content
|
||||
text = `**Action:**\n\n\`\`\`python\n${action.payload.args.browser_actions}\n\`\`\``;
|
||||
} else if (actionID === "recall") {
|
||||
// skip recall actions
|
||||
return;
|
||||
}
|
||||
if (actionID === "run" || actionID === "run_ipython") {
|
||||
if (
|
||||
@@ -143,6 +148,82 @@ export const chatSlice = createSlice({
|
||||
if (!HANDLED_ACTIONS.includes(observationID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for RecallObservation - create a new message instead of updating an existing one
|
||||
if (observationID === "recall") {
|
||||
const recallObs = observation.payload as RecallObservation;
|
||||
let content = ``;
|
||||
|
||||
// Handle workspace context
|
||||
if (recallObs.extras.recall_type === "workspace_context") {
|
||||
if (recallObs.extras.repo_name) {
|
||||
content += `\n\n**Repository:** ${recallObs.extras.repo_name}`;
|
||||
}
|
||||
if (recallObs.extras.repo_directory) {
|
||||
content += `\n\n**Directory:** ${recallObs.extras.repo_directory}`;
|
||||
}
|
||||
if (recallObs.extras.date) {
|
||||
content += `\n\n**Date:** ${recallObs.extras.date}`;
|
||||
}
|
||||
if (
|
||||
recallObs.extras.runtime_hosts &&
|
||||
Object.keys(recallObs.extras.runtime_hosts).length > 0
|
||||
) {
|
||||
content += `\n\n**MicroAgent: Available Hosts**`;
|
||||
for (const [host, port] of Object.entries(
|
||||
recallObs.extras.runtime_hosts,
|
||||
)) {
|
||||
content += `\n\n- ${host} (port ${port})`;
|
||||
}
|
||||
}
|
||||
if (recallObs.extras.repo_instructions) {
|
||||
content += `\n\n**Repository Instructions:**\n\n${recallObs.extras.repo_instructions}`;
|
||||
}
|
||||
if (recallObs.extras.additional_agent_instructions) {
|
||||
content += `\n\n**Additional Instructions:**\n\n${recallObs.extras.additional_agent_instructions}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new message for the observation
|
||||
// Use the correct translation ID format that matches what's in the i18n file
|
||||
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
|
||||
|
||||
// Handle microagent knowledge and prepare custom title if needed
|
||||
let customTitle = translationID;
|
||||
if (
|
||||
recallObs.extras.microagent_knowledge &&
|
||||
recallObs.extras.microagent_knowledge.length > 0
|
||||
) {
|
||||
// Extract microagent names for the title
|
||||
const microagentNames = recallObs.extras.microagent_knowledge
|
||||
.map((k) => k.name)
|
||||
.join(", ");
|
||||
|
||||
// Create custom title with microagent names
|
||||
customTitle = `${translationID}: ${microagentNames}`;
|
||||
|
||||
content += `\n\n**Triggered Microagent Knowledge:**`;
|
||||
for (const knowledge of recallObs.extras.microagent_knowledge) {
|
||||
content += `\n\n- **${knowledge.name}** (triggered by: ${knowledge.trigger})\n\n\`\`\`\n${knowledge.content}\n\`\`\``;
|
||||
}
|
||||
}
|
||||
|
||||
const message: Message = {
|
||||
type: "action",
|
||||
sender: "assistant",
|
||||
translationID: customTitle,
|
||||
eventID: observation.payload.id,
|
||||
content,
|
||||
imageUrls: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
success: true,
|
||||
};
|
||||
|
||||
state.messages.push(message);
|
||||
return; // Skip the normal observation handling below
|
||||
}
|
||||
|
||||
// Normal handling for other observation types
|
||||
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
|
||||
const causeID = observation.payload.cause;
|
||||
const causeMessage = state.messages.find(
|
||||
@@ -203,6 +284,7 @@ export const chatSlice = createSlice({
|
||||
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...(truncated)`;
|
||||
}
|
||||
causeMessage.content = content;
|
||||
// RecallObservation is now handled at the beginning of the function
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
type SliceState = {
|
||||
files: string[]; // base64 encoded images
|
||||
initialPrompt: string | null;
|
||||
selectedRepository: GitRepository | null;
|
||||
selectedRepositoryProvider: Provider | null;
|
||||
selectedRepository: string | null;
|
||||
replayJson: string | null;
|
||||
};
|
||||
|
||||
@@ -14,7 +11,6 @@ const initialState: SliceState = {
|
||||
files: [],
|
||||
initialPrompt: null,
|
||||
selectedRepository: null,
|
||||
selectedRepositoryProvider: null,
|
||||
replayJson: null,
|
||||
};
|
||||
|
||||
@@ -37,7 +33,7 @@ export const selectedFilesSlice = createSlice({
|
||||
clearInitialPrompt(state) {
|
||||
state.initialPrompt = null;
|
||||
},
|
||||
setSelectedRepository(state, action: PayloadAction<GitRepository | null>) {
|
||||
setSelectedRepository(state, action: PayloadAction<string | null>) {
|
||||
state.selectedRepository = action.payload;
|
||||
},
|
||||
clearSelectedRepository(state) {
|
||||
|
||||
@@ -2,14 +2,14 @@ enum ArgConfigType {
|
||||
LLM_MODEL = "LLM_MODEL",
|
||||
AGENT = "AGENT",
|
||||
LANGUAGE = "LANGUAGE",
|
||||
LLM_API_KEY_SET = "LLM_API_KEY_SET",
|
||||
LLM_API_KEY = "LLM_API_KEY",
|
||||
}
|
||||
|
||||
const SupportedSettings: string[] = [
|
||||
ArgConfigType.LLM_MODEL,
|
||||
ArgConfigType.AGENT,
|
||||
ArgConfigType.LANGUAGE,
|
||||
ArgConfigType.LLM_API_KEY_SET,
|
||||
ArgConfigType.LLM_API_KEY,
|
||||
];
|
||||
|
||||
export { ArgConfigType, SupportedSettings };
|
||||
|
||||
@@ -133,6 +133,15 @@ export interface RejectAction extends OpenHandsActionEvent<"reject"> {
|
||||
};
|
||||
}
|
||||
|
||||
export interface RecallAction extends OpenHandsActionEvent<"recall"> {
|
||||
source: "agent";
|
||||
args: {
|
||||
recall_type: "workspace_context" | "knowledge";
|
||||
query: string;
|
||||
thought: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenHandsAction =
|
||||
| UserMessageAction
|
||||
| AssistantMessageAction
|
||||
@@ -146,4 +155,5 @@ export type OpenHandsAction =
|
||||
| FileReadAction
|
||||
| FileEditAction
|
||||
| FileWriteAction
|
||||
| RejectAction;
|
||||
| RejectAction
|
||||
| RecallAction;
|
||||
|
||||
@@ -12,7 +12,8 @@ export type OpenHandsEventType =
|
||||
| "reject"
|
||||
| "think"
|
||||
| "finish"
|
||||
| "error";
|
||||
| "error"
|
||||
| "recall";
|
||||
|
||||
interface OpenHandsBaseEvent {
|
||||
id: number;
|
||||
|
||||
@@ -109,6 +109,26 @@ export interface AgentThinkObservation
|
||||
};
|
||||
}
|
||||
|
||||
export interface MicroagentKnowledge {
|
||||
name: string;
|
||||
trigger: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface RecallObservation extends OpenHandsObservationEvent<"recall"> {
|
||||
source: "agent";
|
||||
extras: {
|
||||
recall_type?: "workspace_context" | "knowledge";
|
||||
repo_name?: string;
|
||||
repo_directory?: string;
|
||||
repo_instructions?: string;
|
||||
runtime_hosts?: Record<string, number>;
|
||||
additional_agent_instructions?: string;
|
||||
date?: string;
|
||||
microagent_knowledge?: MicroagentKnowledge[];
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenHandsObservation =
|
||||
| AgentStateChangeObservation
|
||||
| AgentThinkObservation
|
||||
@@ -120,4 +140,5 @@ export type OpenHandsObservation =
|
||||
| WriteObservation
|
||||
| ReadObservation
|
||||
| EditObservation
|
||||
| ErrorObservation;
|
||||
| ErrorObservation
|
||||
| RecallObservation;
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface InitConfig {
|
||||
AGENT: string;
|
||||
CONFIRMATION_MODE: boolean;
|
||||
LANGUAGE: string;
|
||||
LLM_API_KEY_SET: boolean;
|
||||
LLM_API_KEY: string;
|
||||
LLM_MODEL: string;
|
||||
};
|
||||
token?: string;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitHubErrorReponse {
|
||||
message: string;
|
||||
documentation_url: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface GitUser {
|
||||
interface GitHubUser {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
@@ -15,10 +13,9 @@ interface GitUser {
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
interface GitRepository {
|
||||
interface GitHubRepository {
|
||||
id: number;
|
||||
full_name: string;
|
||||
git_provider: Provider;
|
||||
stargazers_count?: number;
|
||||
link_header?: string;
|
||||
}
|
||||
@@ -29,6 +29,9 @@ enum ObservationType {
|
||||
// A response to the agent's thought (usually a static message)
|
||||
THINK = "think",
|
||||
|
||||
// An observation that shows agent's context extension
|
||||
RECALL = "recall",
|
||||
|
||||
// A no-op observation
|
||||
NULL = "null",
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
export const ProviderOptions = {
|
||||
github: "github",
|
||||
gitlab: "gitlab",
|
||||
} as const;
|
||||
|
||||
export type Provider = keyof typeof ProviderOptions;
|
||||
export type Provider = "github" | "gitlab";
|
||||
|
||||
export type Settings = {
|
||||
LLM_MODEL: string;
|
||||
LLM_BASE_URL: string;
|
||||
AGENT: string;
|
||||
LANGUAGE: string;
|
||||
LLM_API_KEY_SET: boolean;
|
||||
LLM_API_KEY: string | null;
|
||||
CONFIRMATION_MODE: boolean;
|
||||
SECURITY_ANALYZER: string;
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
|
||||
PROVIDER_TOKENS_SET: Record<Provider, boolean>;
|
||||
GITHUB_TOKEN_IS_SET: boolean;
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
ENABLE_SOUND_NOTIFICATIONS: boolean;
|
||||
USER_CONSENTS_TO_ANALYTICS: boolean | null;
|
||||
@@ -28,21 +23,19 @@ export type ApiSettings = {
|
||||
agent: string;
|
||||
language: string;
|
||||
llm_api_key: string | null;
|
||||
llm_api_key_set: boolean;
|
||||
confirmation_mode: boolean;
|
||||
security_analyzer: string;
|
||||
remote_runtime_resource_factor: number | null;
|
||||
github_token_is_set: boolean;
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens_set: Record<Provider, boolean>;
|
||||
};
|
||||
|
||||
export type PostSettings = Settings & {
|
||||
provider_tokens: Record<Provider, string>;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
llm_api_key?: string | null;
|
||||
};
|
||||
|
||||
export type PostApiSettings = ApiSettings & {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { Settings } from "#/types/settings";
|
||||
|
||||
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
|
||||
export const hasAdvancedSettingsSet = (settings: Settings): boolean =>
|
||||
!!settings.LLM_BASE_URL ||
|
||||
settings.AGENT !== DEFAULT_SETTINGS.AGENT ||
|
||||
settings.REMOTE_RUNTIME_RESOURCE_FACTOR !==
|
||||
|
||||
@@ -47,9 +47,7 @@ const extractAdvancedFormData = (formData: FormData) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const extractSettings = (
|
||||
formData: FormData,
|
||||
): Partial<Settings> & { llm_api_key?: string | null } => {
|
||||
export const extractSettings = (formData: FormData): Partial<Settings> => {
|
||||
const { LLM_MODEL, LLM_API_KEY, AGENT, LANGUAGE } =
|
||||
extractBasicFormData(formData);
|
||||
|
||||
@@ -75,7 +73,7 @@ export const extractSettings = (
|
||||
|
||||
return {
|
||||
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
|
||||
LLM_API_KEY_SET: !!LLM_API_KEY,
|
||||
LLM_API_KEY,
|
||||
AGENT,
|
||||
LANGUAGE,
|
||||
LLM_BASE_URL,
|
||||
@@ -83,6 +81,5 @@ export const extractSettings = (
|
||||
SECURITY_ANALYZER,
|
||||
ENABLE_DEFAULT_CONDENSER,
|
||||
PROVIDER_TOKENS: providerTokens,
|
||||
llm_api_key: LLM_API_KEY,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export function renderWithProviders(
|
||||
function Wrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AuthProvider initialProviderTokens={[]}>
|
||||
<AuthProvider initialGithubTokenIsSet>
|
||||
<QueryClientProvider
|
||||
client={
|
||||
new QueryClient({
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: gitlab
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- gitlab
|
||||
- git
|
||||
---
|
||||
|
||||
You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
|
||||
the GitLab API.
|
||||
|
||||
You can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.
|
||||
ALWAYS use the GitLab API for operations instead of a web browser.
|
||||
|
||||
If you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITLAB_TOKEN}@gitlab.com/username/repo.git`
|
||||
|
||||
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
||||
* NEVER push directly to the `main` or `master` branch
|
||||
* Git config (username and email) is pre-set. Do not modify.
|
||||
* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
|
||||
* Use the GitLab API to create a merge request, if you haven't already
|
||||
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* After opening or updating a merge request, send the user a short message with a link to the merge request.
|
||||
* Prefer "Draft" merge requests when possible
|
||||
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
||||
curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/merge_requests" \
|
||||
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
||||
-d '{"source_branch": "create-widget", "target_branch": "openhands-workspace", "title": "Create widget"}'
|
||||
```
|
||||
@@ -490,15 +490,8 @@ class AgentController:
|
||||
|
||||
if self.get_agent_state() != AgentState.RUNNING:
|
||||
await self.set_agent_state_to(AgentState.RUNNING)
|
||||
elif action.source == EventSource.AGENT:
|
||||
# Check if we need to trigger microagents based on agent message content
|
||||
recall_action = RecallAction(
|
||||
query=action.content, recall_type=RecallType.KNOWLEDGE
|
||||
)
|
||||
self._pending_action = recall_action
|
||||
# This is source=AGENT because the agent message is the trigger for the microagent retrieval
|
||||
self.event_stream.add_event(recall_action, EventSource.AGENT)
|
||||
|
||||
elif action.source == EventSource.AGENT:
|
||||
# If the agent is waiting for a response, set the appropriate state
|
||||
if action.wait_for_response:
|
||||
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
|
||||
|
||||
@@ -23,7 +23,6 @@ from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
from openhands.storage import get_file_store
|
||||
from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync
|
||||
|
||||
|
||||
def create_runtime(
|
||||
@@ -118,8 +117,10 @@ def initialize_repository_for_runtime(
|
||||
repo_directory = None
|
||||
if selected_repository and provider_tokens:
|
||||
logger.debug(f'Selected repository {selected_repository}.')
|
||||
repo_directory = call_async_from_sync(
|
||||
runtime.clone_repo, GENERAL_TIMEOUT, github_token, selected_repository, None
|
||||
repo_directory = runtime.clone_repo(
|
||||
provider_tokens,
|
||||
selected_repository,
|
||||
None,
|
||||
)
|
||||
# Run setup script if it exists
|
||||
runtime.maybe_run_setup_script()
|
||||
|
||||
@@ -9,7 +9,6 @@ from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
GitService,
|
||||
ProviderType,
|
||||
Repository,
|
||||
SuggestedTask,
|
||||
TaskType,
|
||||
@@ -100,46 +99,29 @@ class GitHubService(GitService):
|
||||
)
|
||||
|
||||
async def get_repositories(
|
||||
self, sort: str, installation_id: int | None
|
||||
self, page: int, per_page: int, sort: str, installation_id: int | None
|
||||
) -> list[Repository]:
|
||||
MAX_REPOS = 1000
|
||||
PER_PAGE = 100 # Maximum allowed by GitHub API
|
||||
all_repos: list[dict]= []
|
||||
page = 1
|
||||
params = {'page': str(page), 'per_page': str(per_page)}
|
||||
if installation_id:
|
||||
url = f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
response = response.get('repositories', [])
|
||||
else:
|
||||
url = f'{self.BASE_URL}/user/repos'
|
||||
params['sort'] = sort
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
|
||||
while len(all_repos) < MAX_REPOS:
|
||||
params = {'page': str(page), 'per_page': str(PER_PAGE)}
|
||||
if installation_id:
|
||||
url = f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
response = response.get('repositories', [])
|
||||
else:
|
||||
url = f'{self.BASE_URL}/user/repos'
|
||||
params['sort'] = sort
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
|
||||
if not response: # No more repositories
|
||||
break
|
||||
|
||||
all_repos.extend(response)
|
||||
page += 1
|
||||
|
||||
# Check if we've reached the last page
|
||||
link_header = headers.get('Link', '')
|
||||
if 'rel="next"' not in link_header:
|
||||
break
|
||||
|
||||
# Trim to MAX_REPOS if needed and convert to Repository objects
|
||||
all_repos = all_repos[:MAX_REPOS]
|
||||
return [
|
||||
next_link: str = headers.get('Link', '')
|
||||
repos = [
|
||||
Repository(
|
||||
id=repo.get('id'),
|
||||
full_name=repo.get('full_name'),
|
||||
stargazers_count=repo.get('stargazers_count'),
|
||||
git_provider=ProviderType.GITHUB
|
||||
link_header=next_link,
|
||||
)
|
||||
for repo in all_repos
|
||||
for repo in response
|
||||
]
|
||||
return repos
|
||||
|
||||
async def get_installation_ids(self) -> list[int]:
|
||||
url = f'{self.BASE_URL}/user/installations'
|
||||
@@ -161,7 +143,6 @@ class GitHubService(GitService):
|
||||
id=repo.get('id'),
|
||||
full_name=repo.get('full_name'),
|
||||
stargazers_count=repo.get('stargazers_count'),
|
||||
git_provider=ProviderType.GITHUB
|
||||
)
|
||||
for repo in repos
|
||||
]
|
||||
@@ -309,14 +290,6 @@ class GitHubService(GitService):
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def does_repo_exist(self, repository: str) -> bool:
|
||||
url = f'{self.BASE_URL}/repos/{repository}'
|
||||
try:
|
||||
await self._fetch_data(url)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
github_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITHUB_SERVICE_CLS',
|
||||
|
||||
@@ -2,13 +2,11 @@ import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from urllib.parse import quote_plus
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
GitService,
|
||||
ProviderType,
|
||||
Repository,
|
||||
UnknownException,
|
||||
User,
|
||||
@@ -97,7 +95,7 @@ class GitLabService(GitService):
|
||||
|
||||
async def search_repositories(
|
||||
self, query: str, per_page: int = 30, sort: str = 'updated', order: str = 'desc'
|
||||
) -> list[Repository]:
|
||||
):
|
||||
url = f'{self.BASE_URL}/search'
|
||||
params = {
|
||||
'scope': 'projects',
|
||||
@@ -106,82 +104,13 @@ class GitLabService(GitService):
|
||||
'order_by': sort,
|
||||
'sort': order,
|
||||
}
|
||||
response, _ = await self._fetch_data(url, params)
|
||||
repos = [
|
||||
Repository(
|
||||
id=repo.get('id'),
|
||||
full_name=repo.get('path_with_namespace'),
|
||||
stargazers_count=repo.get('star_count'),
|
||||
git_provider=ProviderType.GITLAB
|
||||
)
|
||||
for repo in response
|
||||
]
|
||||
|
||||
return repos
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
return response, headers
|
||||
|
||||
async def get_repositories(
|
||||
self, sort: str, installation_id: int | None
|
||||
self, page: int, per_page: int, sort: str, installation_id: int | None
|
||||
) -> list[Repository]:
|
||||
if installation_id:
|
||||
return [] # Not implementing installation_token case yet
|
||||
|
||||
MAX_REPOS = 1000
|
||||
PER_PAGE = 100 # Maximum allowed by GitLab API
|
||||
all_repos: list[dict] = []
|
||||
page = 1
|
||||
|
||||
url = f'{self.BASE_URL}/projects'
|
||||
# Map GitHub's sort values to GitLab's order_by values
|
||||
order_by = {
|
||||
'pushed': 'last_activity_at',
|
||||
'updated': 'last_activity_at',
|
||||
'created': 'created_at',
|
||||
'full_name': 'name'
|
||||
}.get(sort, 'last_activity_at')
|
||||
|
||||
while len(all_repos) < MAX_REPOS:
|
||||
params = {
|
||||
'page': str(page),
|
||||
'per_page': str(PER_PAGE),
|
||||
'order_by': order_by,
|
||||
'sort': 'desc', # GitLab uses sort for direction (asc/desc)
|
||||
'owned': 1, # Use 1 instead of True
|
||||
'membership': 1 # Use 1 instead of True
|
||||
}
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
|
||||
if not response: # No more repositories
|
||||
break
|
||||
|
||||
all_repos.extend(response)
|
||||
page += 1
|
||||
|
||||
# Check if we've reached the last page
|
||||
link_header = headers.get('Link', '')
|
||||
if 'rel="next"' not in link_header:
|
||||
break
|
||||
|
||||
# Trim to MAX_REPOS if needed and convert to Repository objects
|
||||
all_repos = all_repos[:MAX_REPOS]
|
||||
return [
|
||||
Repository(
|
||||
id=repo.get('id'),
|
||||
full_name=repo.get('path_with_namespace'),
|
||||
stargazers_count=repo.get('star_count'),
|
||||
git_provider=ProviderType.GITLAB
|
||||
)
|
||||
for repo in all_repos
|
||||
]
|
||||
|
||||
async def does_repo_exist(self, repository: str) -> bool:
|
||||
encoded_repo = quote_plus(repository)
|
||||
url = f'{self.BASE_URL}/projects/{encoded_repo}'
|
||||
try:
|
||||
await self._fetch_data(url)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return False
|
||||
return []
|
||||
|
||||
|
||||
gitlab_service_cls = os.environ.get(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from types import MappingProxyType
|
||||
from typing import Annotated, Any, Coroutine, Literal, overload
|
||||
|
||||
@@ -23,12 +24,16 @@ from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
GitService,
|
||||
ProviderType,
|
||||
Repository,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
class ProviderType(Enum):
|
||||
GITHUB = 'github'
|
||||
GITLAB = 'gitlab'
|
||||
|
||||
|
||||
class ProviderToken(BaseModel):
|
||||
token: SecretStr | None = Field(default=None)
|
||||
user_id: str | None = Field(default=None)
|
||||
@@ -189,71 +194,21 @@ class ProviderHandler:
|
||||
return await service.get_latest_token()
|
||||
|
||||
async def get_repositories(
|
||||
self,
|
||||
sort: str,
|
||||
installation_id: int | None,
|
||||
self, page: int, per_page: int, sort: str, installation_id: int | None
|
||||
) -> list[Repository]:
|
||||
"""
|
||||
Get repositories from a selected providers with pagination support
|
||||
"""
|
||||
|
||||
all_repos: list[Repository] = []
|
||||
"""Get repositories from all available providers"""
|
||||
all_repos = []
|
||||
for provider in self.provider_tokens:
|
||||
try:
|
||||
service = self._get_service(provider)
|
||||
service_repos = await service.get_repositories(sort, installation_id)
|
||||
all_repos.extend(service_repos)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return all_repos
|
||||
|
||||
async def search_repositories(
|
||||
self,
|
||||
query: str,
|
||||
per_page: int,
|
||||
sort: str,
|
||||
order: str,
|
||||
):
|
||||
all_repos: list[Repository] = []
|
||||
for provider in self.provider_tokens:
|
||||
try:
|
||||
service = self._get_service(provider)
|
||||
service_repos = await service.search_repositories(
|
||||
query, per_page, sort, order
|
||||
repos = await service.get_repositories(
|
||||
page, per_page, sort, installation_id
|
||||
)
|
||||
all_repos.extend(service_repos)
|
||||
all_repos.extend(repos)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return all_repos
|
||||
|
||||
async def get_remote_repository_url(self, repository: str) -> str | None:
|
||||
if not repository:
|
||||
return None
|
||||
|
||||
provider_domains = {
|
||||
ProviderType.GITHUB: 'github.com',
|
||||
ProviderType.GITLAB: 'gitlab.com',
|
||||
}
|
||||
|
||||
for provider in self.provider_tokens:
|
||||
try:
|
||||
service = self._get_service(provider)
|
||||
repo_exists = await service.does_repo_exist(repository)
|
||||
if repo_exists:
|
||||
git_token = self.provider_tokens[provider].token
|
||||
if git_token and provider in provider_domains:
|
||||
domain = provider_domains[provider]
|
||||
|
||||
if provider == ProviderType.GITLAB:
|
||||
return f'https://oauth2:{git_token.get_secret_value()}@{domain}/{repository}.git'
|
||||
|
||||
return f'https://{git_token.get_secret_value()}@{domain}/{repository}.git'
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
async def set_event_stream_secrets(
|
||||
self,
|
||||
event_stream: EventStream,
|
||||
|
||||
@@ -4,11 +4,6 @@ from typing import Protocol
|
||||
from pydantic import BaseModel, SecretStr
|
||||
|
||||
|
||||
class ProviderType(Enum):
|
||||
GITHUB = 'github'
|
||||
GITLAB = 'gitlab'
|
||||
|
||||
|
||||
class TaskType(str, Enum):
|
||||
MERGE_CONFLICTS = 'MERGE_CONFLICTS'
|
||||
FAILING_CHECKS = 'FAILING_CHECKS'
|
||||
@@ -36,10 +31,8 @@ class User(BaseModel):
|
||||
class Repository(BaseModel):
|
||||
id: int
|
||||
full_name: str
|
||||
git_provider: ProviderType
|
||||
stargazers_count: int | None = None
|
||||
link_header: str | None = None
|
||||
pushed_at: str | None = None # ISO 8601 format date string
|
||||
|
||||
|
||||
class AuthenticationError(ValueError):
|
||||
@@ -88,11 +81,10 @@ class GitService(Protocol):
|
||||
|
||||
async def get_repositories(
|
||||
self,
|
||||
page: int,
|
||||
per_page: int,
|
||||
sort: str,
|
||||
installation_id: int | None,
|
||||
) -> list[Repository]:
|
||||
"""Get repositories for the authenticated user"""
|
||||
...
|
||||
|
||||
async def does_repo_exist(self, repository: str) -> bool:
|
||||
"""Check if a repository exists for the user"""
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Generator
|
||||
|
||||
from litellm import ModelResponse
|
||||
|
||||
from openhands.core.config.agent_config import AgentConfig
|
||||
@@ -127,7 +125,7 @@ class ConversationMemory:
|
||||
pending_tool_call_action_messages.pop(response_id)
|
||||
|
||||
messages += messages_to_add
|
||||
messages = list(ConversationMemory._filter_unmatched_tool_calls(messages))
|
||||
|
||||
return messages
|
||||
|
||||
def process_initial_messages(self, with_caching: bool = False) -> list[Message]:
|
||||
@@ -594,58 +592,3 @@ class ConversationMemory:
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _filter_unmatched_tool_calls(
|
||||
messages: list[Message],
|
||||
) -> Generator[Message, None, None]:
|
||||
"""Filter out tool calls that don't have matching tool responses and vice versa.
|
||||
|
||||
This ensures that every tool_call_id in a tool message has a corresponding tool_calls[].id
|
||||
in an assistant message, and vice versa. The original list is unmodified, when tool_calls is
|
||||
updated the message is copied.
|
||||
|
||||
This does not remove items with id set to None.
|
||||
"""
|
||||
tool_call_ids = {
|
||||
tool_call.id
|
||||
for message in messages
|
||||
if message.tool_calls
|
||||
for tool_call in message.tool_calls
|
||||
if message.role == 'assistant' and tool_call.id
|
||||
}
|
||||
tool_response_ids = {
|
||||
message.tool_call_id
|
||||
for message in messages
|
||||
if message.role == 'tool' and message.tool_call_id
|
||||
}
|
||||
|
||||
for message in messages:
|
||||
# Remove tool messages with no matching assistant tool call
|
||||
if message.role == 'tool' and message.tool_call_id:
|
||||
if message.tool_call_id in tool_call_ids:
|
||||
yield message
|
||||
|
||||
# Remove assistant tool calls with no matching tool response
|
||||
elif message.role == 'assistant' and message.tool_calls:
|
||||
all_tool_calls_match = all(
|
||||
tool_call.id in tool_response_ids
|
||||
for tool_call in message.tool_calls
|
||||
)
|
||||
if all_tool_calls_match:
|
||||
yield message
|
||||
else:
|
||||
matched_tool_calls = [
|
||||
tool_call
|
||||
for tool_call in message.tool_calls
|
||||
if tool_call.id in tool_response_ids
|
||||
]
|
||||
|
||||
if matched_tool_calls:
|
||||
# Keep an updated message if there are tools calls left
|
||||
yield message.model_copy(
|
||||
update={'tool_calls': matched_tool_calls}
|
||||
)
|
||||
else:
|
||||
# Any other case is kept
|
||||
yield message
|
||||
|
||||
@@ -328,7 +328,7 @@ def main() -> None:
|
||||
|
||||
runtime_container_image = my_args.runtime_container_image
|
||||
if runtime_container_image is None:
|
||||
runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.31.0-nikolaik'
|
||||
runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.30.0-nikolaik'
|
||||
|
||||
owner, repo = my_args.selected_repo.split('/')
|
||||
token = my_args.token or os.getenv('GITHUB_TOKEN') or os.getenv('GITLAB_TOKEN')
|
||||
|
||||
@@ -153,7 +153,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
)
|
||||
|
||||
self.user_id = user_id
|
||||
self.git_provider_tokens = git_provider_tokens
|
||||
|
||||
# TODO: remove once done debugging expired github token
|
||||
self.prev_token: SecretStr | None = None
|
||||
@@ -322,25 +321,28 @@ class Runtime(FileEditRuntimeMixin):
|
||||
return
|
||||
self.event_stream.add_event(observation, source) # type: ignore[arg-type]
|
||||
|
||||
async def clone_repo(
|
||||
def clone_repo(
|
||||
self,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE,
|
||||
selected_repository: str,
|
||||
selected_branch: str | None,
|
||||
) -> str:
|
||||
provider_handler = ProviderHandler(provider_tokens=git_provider_tokens)
|
||||
remote_repo_url = await provider_handler.get_remote_repository_url(
|
||||
selected_repository
|
||||
)
|
||||
|
||||
if not remote_repo_url:
|
||||
raise ValueError('Missing either Git token or valid repository')
|
||||
if (
|
||||
ProviderType.GITHUB not in git_provider_tokens
|
||||
or not git_provider_tokens[ProviderType.GITHUB].token
|
||||
or not selected_repository
|
||||
):
|
||||
raise ValueError(
|
||||
'github_token and selected_repository must be provided to clone a repository'
|
||||
)
|
||||
|
||||
if self.status_callback:
|
||||
self.status_callback(
|
||||
'info', 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...'
|
||||
)
|
||||
|
||||
github_token: SecretStr = git_provider_tokens[ProviderType.GITHUB].token
|
||||
url = f'https://{github_token.get_secret_value()}@github.com/{selected_repository}.git'
|
||||
dir_name = selected_repository.split('/')[-1]
|
||||
|
||||
# Generate a random branch name to avoid conflicts
|
||||
@@ -350,7 +352,7 @@ class Runtime(FileEditRuntimeMixin):
|
||||
openhands_workspace_branch = f'openhands-workspace-{random_str}'
|
||||
|
||||
# Clone repository command
|
||||
clone_command = f'git clone {remote_repo_url} {dir_name}'
|
||||
clone_command = f'git clone {url} {dir_name}'
|
||||
|
||||
# Checkout to appropriate branch
|
||||
checkout_command = (
|
||||
|
||||
@@ -2,8 +2,6 @@ from functools import lru_cache
|
||||
from typing import Callable
|
||||
from uuid import UUID
|
||||
|
||||
import os
|
||||
|
||||
import docker
|
||||
import httpx
|
||||
import tenacity
|
||||
@@ -89,10 +87,6 @@ class DockerRuntime(ActionExecutionClient):
|
||||
self._vscode_port = -1
|
||||
self._app_ports: list[int] = []
|
||||
|
||||
if os.environ.get("DOCKER_HOST_ADDR"):
|
||||
logger.info(f'Using DOCKER_HOST_IP: {os.environ["DOCKER_HOST_ADDR"]} for local_runtime_url')
|
||||
self.config.sandbox.local_runtime_url = f'http://{os.environ["DOCKER_HOST_ADDR"]}'
|
||||
|
||||
self.docker_client: docker.DockerClient = self._init_docker_client()
|
||||
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \
|
||||
|
||||
# Install base system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wget curl sudo apt-utils git jq tmux \
|
||||
{%- if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) -%}
|
||||
@@ -23,10 +22,6 @@ RUN apt-get update && \
|
||||
libgl1-mesa-glx \
|
||||
{% endif -%}
|
||||
libasound2-plugins libatomic1 && \
|
||||
# Remove packages with CVEs and no updates yet, if present
|
||||
(apt-get remove -y libaom3 || true) && \
|
||||
(apt-get remove -y libjxl0.7 || true) && \
|
||||
(apt-get remove -y libopenexr-3-1-30 || true) && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from openhands import __version__
|
||||
from openhands.server.routes.conversation import app as conversation_api_router
|
||||
from openhands.server.routes.feedback import app as feedback_api_router
|
||||
from openhands.server.routes.files import app as files_api_router
|
||||
from openhands.server.routes.git import app as git_api_router
|
||||
from openhands.server.routes.github import app as github_api_router
|
||||
from openhands.server.routes.manage_conversations import (
|
||||
app as manage_conversation_api_router,
|
||||
)
|
||||
@@ -50,5 +50,5 @@ app.include_router(feedback_api_router)
|
||||
app.include_router(conversation_api_router)
|
||||
app.include_router(manage_conversation_api_router)
|
||||
app.include_router(settings_router)
|
||||
app.include_router(git_api_router)
|
||||
app.include_router(github_api_router)
|
||||
app.include_router(trajectory_router)
|
||||
|
||||
@@ -5,9 +5,9 @@ from openhands.server.listen_socket import sio
|
||||
from openhands.server.middleware import (
|
||||
AttachConversationMiddleware,
|
||||
CacheControlMiddleware,
|
||||
GitHubTokenMiddleware,
|
||||
InMemoryRateLimiter,
|
||||
LocalhostCORSMiddleware,
|
||||
ProviderTokenMiddleware,
|
||||
RateLimitMiddleware,
|
||||
)
|
||||
from openhands.server.static import SPAStaticFiles
|
||||
@@ -29,6 +29,6 @@ base_app.add_middleware(
|
||||
rate_limiter=InMemoryRateLimiter(requests=10, seconds=1),
|
||||
)
|
||||
base_app.middleware('http')(AttachConversationMiddleware(base_app))
|
||||
base_app.middleware('http')(ProviderTokenMiddleware(base_app))
|
||||
base_app.middleware('http')(GitHubTokenMiddleware(base_app))
|
||||
|
||||
app = socketio.ASGIApp(sio, other_asgi_app=base_app)
|
||||
|
||||
@@ -12,7 +12,6 @@ from openhands.events.observation import (
|
||||
)
|
||||
from openhands.events.observation.agent import (
|
||||
AgentStateChangedObservation,
|
||||
RecallObservation,
|
||||
)
|
||||
from openhands.events.serialization import event_to_dict
|
||||
from openhands.events.stream import AsyncEventStreamWrapper
|
||||
@@ -65,7 +64,7 @@ async def connect(connection_id: str, environ):
|
||||
logger.info(f'oh_event: {event.__class__.__name__}')
|
||||
if isinstance(
|
||||
event,
|
||||
(NullAction, NullObservation, RecallAction, RecallObservation),
|
||||
(NullAction, NullObservation, RecallAction),
|
||||
):
|
||||
continue
|
||||
elif isinstance(event, AgentStateChangedObservation):
|
||||
|
||||
@@ -185,7 +185,7 @@ class AttachConversationMiddleware(SessionMiddlewareInterface):
|
||||
return response
|
||||
|
||||
|
||||
class ProviderTokenMiddleware(SessionMiddlewareInterface):
|
||||
class GitHubTokenMiddleware(SessionMiddlewareInterface):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
|
||||
@@ -17,29 +17,27 @@ from openhands.integrations.service_types import (
|
||||
)
|
||||
from openhands.server.auth import get_access_token, get_provider_tokens
|
||||
|
||||
app = APIRouter(prefix='/api/user')
|
||||
|
||||
|
||||
from pydantic import BaseModel
|
||||
app = APIRouter(prefix='/api/github')
|
||||
|
||||
|
||||
@app.get('/repositories', response_model=list[Repository])
|
||||
async def get_user_repositories(
|
||||
async def get_github_repositories(
|
||||
page: int = 1,
|
||||
per_page: int = 10,
|
||||
sort: str = 'pushed',
|
||||
installation_id: int | None = None,
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
||||
access_token: SecretStr | None = Depends(get_access_token),
|
||||
):
|
||||
|
||||
if provider_tokens:
|
||||
client = ProviderHandler(
|
||||
provider_tokens=provider_tokens, external_auth_token=access_token
|
||||
if provider_tokens and ProviderType.GITHUB in provider_tokens:
|
||||
token = provider_tokens[ProviderType.GITHUB]
|
||||
client = GithubServiceImpl(
|
||||
user_id=token.user_id, external_auth_token=access_token, token=token.token
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
repos: list[Repository] = await client.get_repositories(
|
||||
sort, installation_id
|
||||
page, per_page, sort, installation_id
|
||||
)
|
||||
return repos
|
||||
|
||||
@@ -56,13 +54,13 @@ async def get_user_repositories(
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content='Git provider token required. (such as GitHub).',
|
||||
content='GitHub token required.',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
|
||||
@app.get('/info', response_model=User)
|
||||
async def get_user(
|
||||
@app.get('/user', response_model=User)
|
||||
async def get_github_user(
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
||||
access_token: SecretStr | None = Depends(get_access_token),
|
||||
):
|
||||
@@ -88,7 +86,7 @@ async def get_user(
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content='Git provider token required. (such as GitHub).',
|
||||
content='GitHub token required.',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
@@ -127,7 +125,7 @@ async def get_github_installation_ids(
|
||||
|
||||
|
||||
@app.get('/search/repositories', response_model=list[Repository])
|
||||
async def search_repositories(
|
||||
async def search_github_repositories(
|
||||
query: str,
|
||||
per_page: int = 5,
|
||||
sort: str = 'stars',
|
||||
@@ -135,10 +133,11 @@ async def search_repositories(
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
||||
access_token: SecretStr | None = Depends(get_access_token),
|
||||
):
|
||||
if provider_tokens and ProviderType.GITHUB in provider_tokens:
|
||||
token = provider_tokens[ProviderType.GITHUB]
|
||||
|
||||
if provider_tokens:
|
||||
client = ProviderHandler(
|
||||
provider_tokens=provider_tokens, external_auth_token=access_token
|
||||
client = GithubServiceImpl(
|
||||
user_id=token.user_id, external_auth_token=access_token, token=token.token
|
||||
)
|
||||
try:
|
||||
repos: list[Repository] = await client.search_repositories(
|
||||
@@ -5,7 +5,6 @@ from fastapi import APIRouter, Body, Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.event import EventSource
|
||||
@@ -35,7 +34,6 @@ from openhands.server.types import LLMAuthenticationError, MissingSettingsError
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
|
||||
from openhands.storage.data_models.conversation_status import ConversationStatus
|
||||
from openhands.utils.async_utils import wait_all
|
||||
from openhands.utils.conversation_summary import generate_conversation_title
|
||||
|
||||
app = APIRouter(prefix='/api')
|
||||
|
||||
@@ -245,6 +243,24 @@ async def get_conversation(
|
||||
try:
|
||||
metadata = await conversation_store.get_metadata(conversation_id)
|
||||
is_running = await conversation_manager.is_agent_loop_running(conversation_id)
|
||||
|
||||
# Check if we need to update the title
|
||||
if is_running and metadata:
|
||||
# Check if the title is a default title (contains the conversation ID)
|
||||
if metadata.title and conversation_id[:5] in metadata.title:
|
||||
# Generate a new title
|
||||
new_title = await auto_generate_title(
|
||||
conversation_id, get_user_id(request)
|
||||
)
|
||||
|
||||
if new_title:
|
||||
# Update the metadata
|
||||
metadata.title = new_title
|
||||
await conversation_store.save_metadata(metadata)
|
||||
|
||||
# Refresh metadata after update
|
||||
metadata = await conversation_store.get_metadata(conversation_id)
|
||||
|
||||
conversation_info = await _get_conversation_info(metadata, is_running)
|
||||
return conversation_info
|
||||
except FileNotFoundError:
|
||||
@@ -295,6 +311,10 @@ async def auto_generate_title(conversation_id: str, user_id: str | None) -> str:
|
||||
break
|
||||
|
||||
if first_user_message:
|
||||
# Try LLM-based title generation first
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.utils.conversation_summary import generate_conversation_title
|
||||
|
||||
# Get LLM config from user settings
|
||||
try:
|
||||
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
|
||||
|
||||
@@ -25,27 +25,13 @@ async def load_settings(request: Request) -> GETSettingsModel | JSONResponse:
|
||||
content={'error': 'Settings not found'},
|
||||
)
|
||||
|
||||
provider_tokens_set = {}
|
||||
|
||||
if bool(user_id):
|
||||
provider_tokens_set[ProviderType.GITHUB.value] = True
|
||||
|
||||
provider_tokens = get_provider_tokens(request)
|
||||
if provider_tokens:
|
||||
all_provider_types = [provider.value for provider in ProviderType]
|
||||
provider_tokens_types = [provider.value for provider in provider_tokens]
|
||||
for provider_type in all_provider_types:
|
||||
if provider_type in provider_tokens_types:
|
||||
provider_tokens_set[provider_type] = True
|
||||
else:
|
||||
provider_tokens_set[provider_type] = False
|
||||
|
||||
github_token_is_set = bool(user_id) or bool(get_provider_tokens(request))
|
||||
settings_with_token_data = GETSettingsModel(
|
||||
**settings.model_dump(exclude='secrets_store'),
|
||||
llm_api_key_set=settings.llm_api_key is not None,
|
||||
provider_tokens_set=provider_tokens_set,
|
||||
github_token_is_set=github_token_is_set,
|
||||
)
|
||||
settings_with_token_data.llm_api_key = None
|
||||
|
||||
settings_with_token_data.llm_api_key = settings.llm_api_key
|
||||
return settings_with_token_data
|
||||
except Exception as e:
|
||||
logger.warning(f'Invalid token: {e}')
|
||||
|
||||
@@ -323,9 +323,12 @@ class AgentSession:
|
||||
return False
|
||||
|
||||
if selected_repository and git_provider_tokens:
|
||||
await self.runtime.clone_repo(git_provider_tokens,
|
||||
selected_repository,
|
||||
selected_branch)
|
||||
await call_sync_from_async(
|
||||
self.runtime.clone_repo,
|
||||
git_provider_tokens,
|
||||
selected_repository,
|
||||
selected_branch,
|
||||
)
|
||||
await call_sync_from_async(self.runtime.maybe_run_setup_script)
|
||||
|
||||
self.logger.debug(
|
||||
|
||||
@@ -19,6 +19,7 @@ from openhands.events.observation import (
|
||||
CmdOutputObservation,
|
||||
NullObservation,
|
||||
)
|
||||
from openhands.events.observation.agent import RecallObservation
|
||||
from openhands.events.observation.error import ErrorObservation
|
||||
from openhands.events.serialization import event_from_dict, event_to_dict
|
||||
from openhands.events.stream import EventStreamSubscriber
|
||||
@@ -199,7 +200,7 @@ class Session:
|
||||
await self.send(event_to_dict(event))
|
||||
# NOTE: ipython observations are not sent here currently
|
||||
elif event.source == EventSource.ENVIRONMENT and isinstance(
|
||||
event, (CmdOutputObservation, AgentStateChangedObservation)
|
||||
event, (CmdOutputObservation, AgentStateChangedObservation, RecallObservation)
|
||||
):
|
||||
# feedback from the environment to agent actions is understood as agent events by the UI
|
||||
event_dict = event_to_dict(event)
|
||||
|
||||
@@ -120,5 +120,4 @@ class GETSettingsModel(Settings):
|
||||
Settings with additional token data for the frontend
|
||||
"""
|
||||
|
||||
provider_tokens_set: dict[str, bool] | None = None
|
||||
llm_api_key_set: bool
|
||||
github_token_is_set: bool | None = None
|
||||
|
||||
58
poetry.lock
generated
58
poetry.lock
generated
@@ -496,18 +496,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.37.24"
|
||||
version = "1.37.23"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "boto3-1.37.24-py3-none-any.whl", hash = "sha256:2f2b8f82a5d7f89283973bf2cab771b90c09348799e78b2a25c60cd22c443514"},
|
||||
{file = "boto3-1.37.24.tar.gz", hash = "sha256:1d3c6fc63a9efba0af8b531ec6b7f7c6b0ef197bf3dcd875f03c9097ac68b58f"},
|
||||
{file = "boto3-1.37.23-py3-none-any.whl", hash = "sha256:fc462b9fd738bd8a1c121d94d237c6b6a05a2c1cc709d16f5223acb752f7310b"},
|
||||
{file = "boto3-1.37.23.tar.gz", hash = "sha256:82f4599a34f5eb66e916b9ac8547394f6e5899c19580e74b60237db04cf66d1e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.37.24,<1.38.0"
|
||||
botocore = ">=1.37.23,<1.38.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.11.0,<0.12.0"
|
||||
|
||||
@@ -516,14 +516,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "boto3-stubs"
|
||||
version = "1.37.24"
|
||||
description = "Type annotations for boto3 1.37.24 generated with mypy-boto3-builder 8.10.1"
|
||||
version = "1.37.23"
|
||||
description = "Type annotations for boto3 1.37.23 generated with mypy-boto3-builder 8.10.1"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "boto3_stubs-1.37.24-py3-none-any.whl", hash = "sha256:0c085621dcfb861be1b3066aaed294eca37a2f99d9e737b41dc2de3a26498c27"},
|
||||
{file = "boto3_stubs-1.37.24.tar.gz", hash = "sha256:42f7c1b3da40eb074ffc830b26417c9af86546a609fd8563d7af4deade3b5194"},
|
||||
{file = "boto3_stubs-1.37.23-py3-none-any.whl", hash = "sha256:a00884a3df819bdc6b040c857e57a87b4f33df963ee88f8f406b13bf2cd983ca"},
|
||||
{file = "boto3_stubs-1.37.23.tar.gz", hash = "sha256:011f06dadcd5ef3c627ec9808b9afa4e1837b0f009d82b8209f12a84ffbb3867"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -579,7 +579,7 @@ bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (
|
||||
bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.37.0,<1.38.0)"]
|
||||
billing = ["mypy-boto3-billing (>=1.37.0,<1.38.0)"]
|
||||
billingconductor = ["mypy-boto3-billingconductor (>=1.37.0,<1.38.0)"]
|
||||
boto3 = ["boto3 (==1.37.24)"]
|
||||
boto3 = ["boto3 (==1.37.23)"]
|
||||
braket = ["mypy-boto3-braket (>=1.37.0,<1.38.0)"]
|
||||
budgets = ["mypy-boto3-budgets (>=1.37.0,<1.38.0)"]
|
||||
ce = ["mypy-boto3-ce (>=1.37.0,<1.38.0)"]
|
||||
@@ -943,14 +943,14 @@ xray = ["mypy-boto3-xray (>=1.37.0,<1.38.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.37.24"
|
||||
version = "1.37.23"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "botocore-1.37.24-py3-none-any.whl", hash = "sha256:f1a55332cca85a6556af8941cccdaf5d2d00336647d9e89f31174f2361ffb4f2"},
|
||||
{file = "botocore-1.37.24.tar.gz", hash = "sha256:a0bcc3c376a371f2c11afcbcc9917010c1c0a701d0e45d1ea3ec3bddeb06a8ff"},
|
||||
{file = "botocore-1.37.23-py3-none-any.whl", hash = "sha256:ffbe1f5958adb1c50d72d3ad1018cb265fe349248c08782d334601c0814f0e38"},
|
||||
{file = "botocore-1.37.23.tar.gz", hash = "sha256:3a249c950cef9ee9ed7b2278500ad83a4ad6456bc433a43abd1864d1b61b2acb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2744,14 +2744,14 @@ tool = ["click (>=6.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-aiplatform"
|
||||
version = "1.87.0"
|
||||
version = "1.86.0"
|
||||
description = "Vertex AI API client library"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "google_cloud_aiplatform-1.87.0-py2.py3-none-any.whl", hash = "sha256:23421cb288550e1b35c5a81f471618e0c34c32dfc73db895498ac01c9a5fa46c"},
|
||||
{file = "google_cloud_aiplatform-1.87.0.tar.gz", hash = "sha256:3628c573d677d72c7ad62e9c898ff7073c915241e86fd563ff2ee581d281e494"},
|
||||
{file = "google_cloud_aiplatform-1.86.0-py2.py3-none-any.whl", hash = "sha256:fcd155a0e77fdc12a5c477af92fa0b7e8ea1b1d1fcece35ad07f160008dedc7e"},
|
||||
{file = "google_cloud_aiplatform-1.86.0.tar.gz", hash = "sha256:45fff84c75c6f66105efa1c6caf0ea87fddc85298c834ee38f4163cf793510c4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4393,14 +4393,14 @@ types-tqdm = "*"
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.65.1"
|
||||
version = "1.65.0"
|
||||
description = "Library to easily interface with LLM API providers"
|
||||
optional = false
|
||||
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "litellm-1.65.1-py3-none-any.whl", hash = "sha256:56478866373c4af13d86e71ab44fd337305dee39a82d742bc496bc56a2d732a1"},
|
||||
{file = "litellm-1.65.1.tar.gz", hash = "sha256:cbc8d7dfa5f7f47e6842796ca0c39682eb874718faab17fa991ab6c2f55e844c"},
|
||||
{file = "litellm-1.65.0-py3-none-any.whl", hash = "sha256:bbc211f3d03e1830ed7f4304b40f70fa1fa4a2f9109d006ede5f78e83a189aba"},
|
||||
{file = "litellm-1.65.0.tar.gz", hash = "sha256:147a74d18601ccaaff3ca125eba914ab6e5b5854aff480dce5a52be5b9d52ff8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4410,7 +4410,7 @@ httpx = ">=0.23.0"
|
||||
importlib-metadata = ">=6.8.0"
|
||||
jinja2 = ">=3.1.2,<4.0.0"
|
||||
jsonschema = ">=4.22.0,<5.0.0"
|
||||
openai = ">=1.68.2"
|
||||
openai = ">=1.66.1"
|
||||
pydantic = ">=2.0.0,<3.0.0"
|
||||
python-dotenv = ">=0.2.0"
|
||||
tiktoken = ">=0.7.0"
|
||||
@@ -4418,7 +4418,7 @@ tokenizers = "*"
|
||||
|
||||
[package.extras]
|
||||
extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"]
|
||||
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.1)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"]
|
||||
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
@@ -4836,14 +4836,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "modal"
|
||||
version = "0.73.138"
|
||||
version = "0.73.136"
|
||||
description = "Python client library for Modal"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "evaluation"]
|
||||
files = [
|
||||
{file = "modal-0.73.138-py3-none-any.whl", hash = "sha256:da60510d789509e1a5c9c4a2458e0295dfbee768eae71f0bba739b99215d2a08"},
|
||||
{file = "modal-0.73.138.tar.gz", hash = "sha256:943daebf7dea70d16b978c633ff3cfc1d453151043f64c7a9012e2526b6be2d2"},
|
||||
{file = "modal-0.73.136-py3-none-any.whl", hash = "sha256:1f812712ea616cce949c06c5a4b45497d1157879775986de54db9ed2023b79e9"},
|
||||
{file = "modal-0.73.136.tar.gz", hash = "sha256:e8a6d3961c11e6440b2ab9a7f344fb1beb9aae8b8511df871ce3b2399f194af0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -5392,14 +5392,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.70.0"
|
||||
version = "1.69.0"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "evaluation", "test"]
|
||||
files = [
|
||||
{file = "openai-1.70.0-py3-none-any.whl", hash = "sha256:f6438d053fd8b2e05fd6bef70871e832d9bbdf55e119d0ac5b92726f1ae6f614"},
|
||||
{file = "openai-1.70.0.tar.gz", hash = "sha256:e52a8d54c3efeb08cf58539b5b21a5abef25368b5432965e4de88cdf4e091b2b"},
|
||||
{file = "openai-1.69.0-py3-none-any.whl", hash = "sha256:73c4b2ddfd050060f8d93c70367189bd891e70a5adb6d69c04c3571f4fea5627"},
|
||||
{file = "openai-1.69.0.tar.gz", hash = "sha256:7b8a10a8ff77e1ae827e5e4c8480410af2070fb68bc973d6c994cf8218f1f98d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -6674,14 +6674,14 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "6.1.0"
|
||||
version = "6.0.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "pytest_cov-6.1.0-py3-none-any.whl", hash = "sha256:cd7e1d54981d5185ef2b8d64b50172ce97e6f357e6df5cb103e828c7f993e201"},
|
||||
{file = "pytest_cov-6.1.0.tar.gz", hash = "sha256:ec55e828c66755e5b74a21bd7cc03c303a9f928389c0563e50ba454a6dbe71db"},
|
||||
{file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"},
|
||||
{file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.31.0"
|
||||
version = "0.30.1"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
@@ -97,6 +97,7 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -125,6 +126,7 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user