mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
47 Commits
0.51.1
...
feature/or
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2ed78ee2 | ||
|
|
cda29107f1 | ||
|
|
97bfa96a15 | ||
|
|
0e2f2f4173 | ||
|
|
5554b7b418 | ||
|
|
d30f77c60a | ||
|
|
a36d1673fa | ||
|
|
d233e89873 | ||
|
|
402b6224a6 | ||
|
|
4e5e2a7095 | ||
|
|
a0adbd741a | ||
|
|
d5cdecea21 | ||
|
|
fef287fcb0 | ||
|
|
6fc1a63eb8 | ||
|
|
5364e2638b | ||
|
|
d3983b00bd | ||
|
|
39fff41dd4 | ||
|
|
d0a8c896c2 | ||
|
|
4f24bcaec9 | ||
|
|
d3209f8c28 | ||
|
|
287c34b3f3 | ||
|
|
1cdc38eafb | ||
|
|
ae045251f2 | ||
|
|
9b374cd6b8 | ||
|
|
4759a78c12 | ||
|
|
d88e68eb49 | ||
|
|
b9abdf10ce | ||
|
|
5b5a9718c2 | ||
|
|
86dac5efe4 | ||
|
|
dfeab9f767 | ||
|
|
4b13658401 | ||
|
|
844b00a380 | ||
|
|
29fe911828 | ||
|
|
5282770a4c | ||
|
|
953902dcce | ||
|
|
b28e0533e0 | ||
|
|
43555fa13b | ||
|
|
10ae481b91 | ||
|
|
c2e860fe92 | ||
|
|
c2fc84e6ea | ||
|
|
6f44b7352e | ||
|
|
16106e6262 | ||
|
|
6cea73b6da | ||
|
|
fdf9a49e28 | ||
|
|
e348634dbd | ||
|
|
b67db15f8a | ||
|
|
5ac1fd9077 |
@@ -34,7 +34,7 @@ _Dev Container: Reopen in Container_ command from the Command Palette
|
||||
|
||||
#### Develop without sudo access
|
||||
|
||||
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
|
||||
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJS`, you can use
|
||||
`conda` or `mamba` to manage the packages for you:
|
||||
|
||||
```bash
|
||||
@@ -71,7 +71,7 @@ This command will prompt you to enter the LLM API key, model name, and other var
|
||||
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
|
||||
please set the model in the UI.
|
||||
|
||||
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
|
||||
Note: If you have previously run OpenHands using the docker command, you may have already set some environment
|
||||
variables in your terminal. The final configurations are set from highest to lowest priority:
|
||||
Environment variables > config.toml variables > default variables
|
||||
|
||||
@@ -154,12 +154,12 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
1. Add your dependency in `pyproject.toml` or use `poetry add xxx`.
|
||||
2. Update the poetry.lock file via `poetry lock --no-update`.
|
||||
|
||||
### 9. Use existing Docker image
|
||||
### 10. Use existing Docker image
|
||||
|
||||
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.50-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.51-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -3,10 +3,10 @@ SHELL=/usr/bin/env bash
|
||||
|
||||
# Variables
|
||||
BACKEND_HOST ?= "127.0.0.1"
|
||||
BACKEND_PORT = 3000
|
||||
BACKEND_PORT ?= 3000
|
||||
BACKEND_HOST_PORT = "$(BACKEND_HOST):$(BACKEND_PORT)"
|
||||
FRONTEND_HOST ?= "127.0.0.1"
|
||||
FRONTEND_PORT = 3001
|
||||
FRONTEND_PORT ?= 3001
|
||||
DEFAULT_WORKSPACE_DIR = "./workspace"
|
||||
DEFAULT_MODEL = "gpt-4o"
|
||||
CONFIG_FILE = config.toml
|
||||
|
||||
@@ -62,17 +62,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.50
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.51
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.50
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.51
|
||||
```
|
||||
|
||||
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
|
||||
|
||||
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
|
||||
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.50
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.51
|
||||
```
|
||||
|
||||
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
|
||||
|
||||
@@ -58,8 +58,8 @@ RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs
|
||||
# Default is 60000, but we've seen up to 200000
|
||||
RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs
|
||||
|
||||
RUN groupadd app
|
||||
RUN useradd -l -m -u $OPENHANDS_USER_ID -s /bin/bash openhands && \
|
||||
RUN groupadd --gid $OPENHANDS_USER_ID app
|
||||
RUN useradd -l -m -u $OPENHANDS_USER_ID --gid $OPENHANDS_USER_ID -s /bin/bash openhands && \
|
||||
usermod -aG app openhands && \
|
||||
usermod -aG sudo openhands && \
|
||||
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
@@ -23,6 +23,18 @@ if [ -z "$WORKSPACE_MOUNT_PATH" ]; then
|
||||
unset WORKSPACE_BASE
|
||||
fi
|
||||
|
||||
if [[ "$INSTALL_THIRD_PARTY_RUNTIMES" == "true" ]]; then
|
||||
echo "Downloading and installing third_party_runtimes..."
|
||||
echo "Warning: Third-party runtimes are provided as-is, not actively supported and may be removed in future releases."
|
||||
|
||||
if pip install 'openhands-ai[third_party_runtimes]' -qqq 2> >(tee /dev/stderr); then
|
||||
echo "third_party_runtimes installed successfully."
|
||||
else
|
||||
echo "Failed to install third_party_runtimes." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
|
||||
echo "Running OpenHands as root"
|
||||
export RUN_AS_OPENHANDS=false
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
- DOCKER_HOST_ADDR=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.50-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.51-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.50-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.51-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 for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -37,7 +37,16 @@
|
||||
"usage/cloud/bitbucket-installation",
|
||||
"usage/cloud/github-installation",
|
||||
"usage/cloud/gitlab-installation",
|
||||
"usage/cloud/slack-installation"
|
||||
"usage/cloud/slack-installation",
|
||||
{
|
||||
"group": "Project Management Tools",
|
||||
"pages": [
|
||||
"usage/cloud/project-management/overview",
|
||||
"usage/cloud/project-management/jira-integration",
|
||||
"usage/cloud/project-management/jira-dc-integration",
|
||||
"usage/cloud/project-management/linear-integration"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage/cloud/cloud-ui",
|
||||
|
||||
BIN
docs/static/img/workspace-admin-edit.png
vendored
Normal file
BIN
docs/static/img/workspace-admin-edit.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/static/img/workspace-configure.png
vendored
Normal file
BIN
docs/static/img/workspace-configure.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/static/img/workspace-link.png
vendored
Normal file
BIN
docs/static/img/workspace-link.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/workspace-user-edit.png
vendored
Normal file
BIN
docs/static/img/workspace-user-edit.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
118
docs/usage/cloud/project-management/jira-dc-integration.mdx
Normal file
118
docs/usage/cloud/project-management/jira-dc-integration.mdx
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Jira Data Center Integration (Beta)
|
||||
description: Complete guide for setting up Jira Data Center integration with OpenHands Cloud, including service account creation, personal access token generation, webhook configuration, and workspace integration setup.
|
||||
---
|
||||
|
||||
# Jira Data Center Integration
|
||||
|
||||
## Platform Configuration
|
||||
|
||||
### Step 1: Create Service Account
|
||||
|
||||
1. **Access User Management**
|
||||
- Log in to Jira Data Center as administrator
|
||||
- Go to **Administration** > **User Management**
|
||||
|
||||
2. **Create User**
|
||||
- Click **Create User**
|
||||
- Username: `openhands-agent`
|
||||
- Full Name: `OpenHands Agent`
|
||||
- Email: `openhands@yourcompany.com` (replace with your preferred service account email)
|
||||
- Password: Set a secure password
|
||||
- Click **Create**
|
||||
|
||||
3. **Assign Permissions**
|
||||
- Add user to appropriate groups
|
||||
- Ensure access to relevant projects
|
||||
- Grant necessary project permissions
|
||||
|
||||
### Step 2: Generate API Token
|
||||
|
||||
1. **Personal Access Tokens**
|
||||
- Log in as the service account
|
||||
- Go to **Profile** > **Personal Access Tokens**
|
||||
- Click **Create token**
|
||||
- Name: `OpenHands Cloud Integration`
|
||||
- Expiry: Set appropriate expiration (recommend 1 year)
|
||||
- Click **Create**
|
||||
- **Important**: Copy and store the token securely
|
||||
|
||||
### Step 3: Configure Webhook
|
||||
|
||||
1. **Create Webhook**
|
||||
- Go to **Administration** > **System** > **WebHooks**
|
||||
- Click **Create a WebHook**
|
||||
- **Name**: `OpenHands Cloud Integration`
|
||||
- **URL**: `https://app.all-hands.dev/integration/jira-dc/events`
|
||||
- Set a suitable webhook secret
|
||||
- **Issue related events**: Select the following:
|
||||
- Issue updated
|
||||
- Comment created
|
||||
- **JQL Filter**: Leave empty (or customize as needed)
|
||||
- Click **Create**
|
||||
- **Important**: Copy and store the webhook secret securely (you'll need this for workspace integration)
|
||||
|
||||
---
|
||||
|
||||
## Workspace Integration
|
||||
|
||||
### Step 1: Log in to OpenHands Cloud
|
||||
|
||||
1. **Navigate and Authenticate**
|
||||
- Go to [OpenHands Cloud](https://app.all-hands.dev/)
|
||||
- Sign in with your Git provider (GitHub, GitLab, or BitBucket)
|
||||
- **Important:** Make sure you're signing in with the same Git provider account that contains the repositories you want the OpenHands agent to work on.
|
||||
|
||||
### Step 2: Configure Jira Data Center Integration
|
||||
|
||||
1. **Access Integration Settings**
|
||||
- Navigate to **Settings** > **Integrations**
|
||||
- Locate **Jira Data Center** section
|
||||
|
||||
2. **Configure Workspace**
|
||||
- Click **Configure** button
|
||||
- Enter your workspace name and click **Connect**
|
||||
- If no integration exists, you'll be prompted to enter additional credentials required for the workspace integration:
|
||||
- **Webhook Secret**: The webhook secret from Step 3 above
|
||||
- **Service Account Email**: The service account email from Step 1 above
|
||||
- **Service Account API Key**: The personal access token from Step 2 above
|
||||
- Ensure **Active** toggle is enabled
|
||||
|
||||
3. **Complete OAuth Flow**
|
||||
- You'll be redirected to Jira Data Center to complete OAuth verification
|
||||
- Grant the necessary permissions to verify your workspace access. If you have access to multiple workspaces, select the correct one that you initially provided
|
||||
- If successful, you will be redirected back to the **Integrations** settings in the OpenHands Cloud UI
|
||||
|
||||
### Managing Your Integration
|
||||
|
||||
**Edit Configuration:**
|
||||
- Click the **Edit** button next to your configured platform
|
||||
- Update any necessary credentials or settings
|
||||
- Click **Update** to apply changes
|
||||
- You will need to repeat the OAuth flow as before
|
||||
- **Important:** Only the original user who created the integration can see the edit view
|
||||
|
||||
**Unlink Workspace:**
|
||||
- In the edit view, click **Unlink** next to the workspace name
|
||||
- This will deactivate your workspace link
|
||||
- **Important:** If the original user who configured the integration chooses to unlink their integration, any users currently linked to that integration will also be unlinked, and the workspace integration will be deactivated. The integration can only be reactivated by the original user.
|
||||
|
||||
### Screenshots
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Workspace link flow">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workspace Configure flow">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as a user">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as the workspace creator">
|
||||

|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
123
docs/usage/cloud/project-management/jira-integration.mdx
Normal file
123
docs/usage/cloud/project-management/jira-integration.mdx
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: Jira Cloud Integration
|
||||
description: Complete guide for setting up Jira Cloud integration with OpenHands Cloud, including service account creation, API token generation, webhook configuration, and workspace integration setup.
|
||||
---
|
||||
|
||||
# Jira Cloud Integration
|
||||
|
||||
## Platform Configuration
|
||||
|
||||
### Step 1: Create Service Account
|
||||
|
||||
1. **Navigate to User Management**
|
||||
- Go to [Atlassian Admin](https://admin.atlassian.com/)
|
||||
- Select your organization
|
||||
- Go to **Directory** > **Users**
|
||||
|
||||
2. **Create OpenHands Service Account**
|
||||
- Click **Add user**
|
||||
- Email: `openhands@yourcompany.com` (replace with your preferred service account email)
|
||||
- Display name: `OpenHands Agent`
|
||||
- Send invitation: **No** (you'll set password manually)
|
||||
- Click **Add user**
|
||||
|
||||
3. **Configure Account**
|
||||
- Locate the created user and click on it
|
||||
- Set a secure password
|
||||
- Add to relevant Jira projects with appropriate permissions
|
||||
|
||||
### Step 2: Generate API Token
|
||||
|
||||
1. **Access API Token Management**
|
||||
- Log in as the OpenHands service account
|
||||
- Go to [API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
|
||||
|
||||
2. **Create API Token**
|
||||
- Click **Create API token**
|
||||
- Label: `OpenHands Cloud Integration`
|
||||
- Expiry: Set appropriate expiration (recommend 1 year)
|
||||
- Click **Create**
|
||||
- **Important**: Copy and securely store the token immediately
|
||||
|
||||
### Step 3: Configure Webhook
|
||||
|
||||
1. **Navigate to Webhook Settings**
|
||||
- Go to **Jira Settings** > **System** > **WebHooks**
|
||||
- Click **Create a WebHook**
|
||||
|
||||
2. **Configure Webhook**
|
||||
- **Name**: `OpenHands Cloud Integration`
|
||||
- **Status**: Enabled
|
||||
- **URL**: `https://app.all-hands.dev/integration/jira/events`
|
||||
- **Issue related events**: Select the following:
|
||||
- Issue updated
|
||||
- Comment created
|
||||
- **JQL Filter**: Leave empty (or customize as needed)
|
||||
- Click **Create**
|
||||
- **Important**: Copy and store the webhook secret securely (you'll need this for workspace integration)
|
||||
|
||||
---
|
||||
|
||||
## Workspace Integration
|
||||
|
||||
### Step 1: Log in to OpenHands Cloud
|
||||
|
||||
1. **Navigate and Authenticate**
|
||||
- Go to [OpenHands Cloud](https://app.all-hands.dev/)
|
||||
- Sign in with your Git provider (GitHub, GitLab, or BitBucket)
|
||||
- **Important:** Make sure you're signing in with the same Git provider account that contains the repositories you want the OpenHands agent to work on.
|
||||
|
||||
### Step 2: Configure Jira Integration
|
||||
|
||||
1. **Access Integration Settings**
|
||||
- Navigate to **Settings** > **Integrations**
|
||||
- Locate **Jira Cloud** section
|
||||
|
||||
2. **Configure Workspace**
|
||||
- Click **Configure** button
|
||||
- Enter your workspace name and click **Connect**
|
||||
- **Important:** Make sure you enter the full workspace name, eg: **yourcompany.atlassian.net**
|
||||
- If no integration exists, you'll be prompted to enter additional credentials required for the workspace integration:
|
||||
- **Webhook Secret**: The webhook secret from Step 3 above
|
||||
- **Service Account Email**: The service account email from Step 1 above
|
||||
- **Service Account API Key**: The API token from Step 2 above
|
||||
- Ensure **Active** toggle is enabled
|
||||
|
||||
3. **Complete OAuth Flow**
|
||||
- You'll be redirected to Jira Cloud to complete OAuth verification
|
||||
- Grant the necessary permissions to verify your workspace access.
|
||||
- If successful, you will be redirected back to the **Integrations** settings in the OpenHands Cloud UI
|
||||
|
||||
### Managing Your Integration
|
||||
|
||||
**Edit Configuration:**
|
||||
- Click the **Edit** button next to your configured platform
|
||||
- Update any necessary credentials or settings
|
||||
- Click **Update** to apply changes
|
||||
- You will need to repeat the OAuth flow as before
|
||||
- **Important:** Only the original user who created the integration can see the edit view
|
||||
|
||||
**Unlink Workspace:**
|
||||
- In the edit view, click **Unlink** next to the workspace name
|
||||
- This will deactivate your workspace link
|
||||
- **Important:** If the original user who configured the integration chooses to unlink their integration, any users currently linked to that workspace integration will also be unlinked, and the workspace integration will be deactivated. The integration can only be reactivated by the original user.
|
||||
|
||||
### Screenshots
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Workspace link flow">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workspace Configure flow">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as a user">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as the workspace creator">
|
||||

|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
122
docs/usage/cloud/project-management/linear-integration.mdx
Normal file
122
docs/usage/cloud/project-management/linear-integration.mdx
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: Linear Integration
|
||||
description: Complete guide for setting up Linear integration with OpenHands Cloud, including service account creation, API key generation, webhook configuration, and workspace integration setup.
|
||||
---
|
||||
|
||||
# Linear Integration
|
||||
|
||||
## Platform Configuration
|
||||
|
||||
### Step 1: Create Service Account
|
||||
|
||||
1. **Access Team Settings**
|
||||
- Log in to Linear as a team admin
|
||||
- Go to **Settings** > **Members**
|
||||
|
||||
2. **Invite Service Account**
|
||||
- Click **Invite members**
|
||||
- Email: `openhands@yourcompany.com` (replace with your preferred service account email)
|
||||
- Role: **Member** (with appropriate team access)
|
||||
- Send invitation
|
||||
|
||||
3. **Complete Setup**
|
||||
- Accept invitation from the service account email
|
||||
- Complete profile setup
|
||||
- Ensure access to relevant teams/workspaces
|
||||
|
||||
### Step 2: Generate API Key
|
||||
|
||||
1. **Access API Settings**
|
||||
- Log in as the service account
|
||||
- Go to **Settings** > **API**
|
||||
|
||||
2. **Create Personal API Key**
|
||||
- Click **Create new key**
|
||||
- Name: `OpenHands Cloud Integration`
|
||||
- Scopes: Select the following:
|
||||
- `Read` - Read access to issues and comments
|
||||
- `Create comments` - Ability to create or update comments
|
||||
- Select the teams you want to provide access to, or allow access for all teams you have permissions for
|
||||
- Click **Create**
|
||||
- **Important**: Copy and store the API key securely
|
||||
|
||||
### Step 3: Configure Webhook
|
||||
|
||||
1. **Access Webhook Settings**
|
||||
- Go to **Settings** > **API** > **Webhooks**
|
||||
- Click **New webhook**
|
||||
|
||||
2. **Configure Webhook**
|
||||
- **Label**: `OpenHands Cloud Integration`
|
||||
- **URL**: `https://app.all-hands.dev/integration/linear/events`
|
||||
- **Resource types**: Select:
|
||||
- `Comment` - For comment events
|
||||
- `Issue` - For issue updates (label changes)
|
||||
- Select the teams you want to provide access to, or allow access for all public teams
|
||||
- Click **Create webhook**
|
||||
- **Important**: Copy and store the webhook secret securely (you'll need this for workspace integration)
|
||||
|
||||
---
|
||||
|
||||
## Workspace Integration
|
||||
|
||||
### Step 1: Log in to OpenHands Cloud
|
||||
|
||||
1. **Navigate and Authenticate**
|
||||
- Go to [OpenHands Cloud](https://app.all-hands.dev/)
|
||||
- Sign in with your Git provider (GitHub, GitLab, or BitBucket)
|
||||
- **Important:** Make sure you're signing in with the same Git provider account that contains the repositories you want the OpenHands agent to work on.
|
||||
|
||||
### Step 2: Configure Linear Integration
|
||||
|
||||
1. **Access Integration Settings**
|
||||
- Navigate to **Settings** > **Integrations**
|
||||
- Locate **Linear** section
|
||||
|
||||
2. **Configure Workspace**
|
||||
- Click **Configure** button
|
||||
- Enter your workspace name and click **Connect**
|
||||
- If no integration exists, you'll be prompted to enter additional credentials required for the workspace integration:
|
||||
- **Webhook Secret**: The webhook secret from Step 3 above
|
||||
- **Service Account Email**: The service account email from Step 1 above
|
||||
- **Service Account API Key**: The API key from Step 2 above
|
||||
- Ensure **Active** toggle is enabled
|
||||
|
||||
3. **Complete OAuth Flow**
|
||||
- You'll be redirected to Linear to complete OAuth verification
|
||||
- Grant the necessary permissions to verify your workspace access. If you have access to multiple workspaces, select the correct one that you initially provided
|
||||
- If successful, you will be redirected back to the **Integrations** settings in the OpenHands Cloud UI
|
||||
|
||||
### Managing Your Integration
|
||||
|
||||
**Edit Configuration:**
|
||||
- Click the **Edit** button next to your configured platform
|
||||
- Update any necessary credentials or settings
|
||||
- Click **Update** to apply changes
|
||||
- You will need to repeat the OAuth flow as before
|
||||
- **Important:** Only the original user who created the integration can see the edit view
|
||||
|
||||
**Unlink Workspace:**
|
||||
- In the edit view, click **Unlink** next to the workspace name
|
||||
- This will deactivate your workspace link
|
||||
- **Important:** If the original user who configured the integration chooses to unlink their integration, any users currently linked to that integration will also be unlinked, and the workspace integration will be deactivated. The integration can only be reactivated by the original user.
|
||||
|
||||
### Screenshots
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Workspace link flow">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workspace Configure flow">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as a user">
|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as the workspace creator">
|
||||

|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
79
docs/usage/cloud/project-management/overview.mdx
Normal file
79
docs/usage/cloud/project-management/overview.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Project Management Tool Integrations
|
||||
description: Overview of OpenHands Cloud integrations with project management platforms including Jira Cloud, Jira Data Center, and Linear. Learn about setup requirements, usage methods, and troubleshooting.
|
||||
---
|
||||
|
||||
# Project Management Tool Integrations
|
||||
|
||||
## Overview
|
||||
|
||||
OpenHands Cloud integrates with project management platforms (Jira Cloud, Jira Data Center, and Linear) to enable AI-powered task delegation. Users can invoke the OpenHands agent by:
|
||||
- Adding `@openhands` in ticket comments
|
||||
- Adding the `openhands` label to tickets
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Integration requires two levels of setup:
|
||||
1. **Platform Configuration** - Administrative setup of service accounts and webhooks on your project management platform (see individual platform documentation below)
|
||||
2. **Workspace Integration** - Self-service configuration through the OpenHands Cloud UI to link your OpenHands account to the target workspace
|
||||
|
||||
### Platform-Specific Setup Guides:
|
||||
- [Jira Cloud Integration](./jira-integration.md)
|
||||
- [Jira Data Center Integration](./jira-dc-integration.md)
|
||||
- [Linear Integration](./linear-integration.md)
|
||||
|
||||
## Usage
|
||||
|
||||
Once both the platform configuration and workspace integration are completed, users can trigger the OpenHands agent within their project management platforms using two methods:
|
||||
|
||||
### Method 1: Comment Mention
|
||||
Add a comment to any issue with `@openhands` followed by your task description:
|
||||
```
|
||||
@openhands Please implement the user authentication feature described in this ticket
|
||||
```
|
||||
|
||||
### Method 2: Label-based Delegation
|
||||
Add the label `openhands` to any issue. The OpenHands agent will automatically process the issue based on its description and requirements.
|
||||
|
||||
### Git Repository Detection
|
||||
|
||||
The OpenHands agent needs to identify which Git repository to work with when processing your issues. Here's how to ensure proper repository detection:
|
||||
|
||||
#### Specifying the Target Repository
|
||||
|
||||
**Required:** Include the target Git repository in your issue description or comment to ensure the agent works with the correct codebase.
|
||||
|
||||
**Supported Repository Formats:**
|
||||
- Full HTTPS URL: `https://github.com/owner/repository.git`
|
||||
- GitHub URL without .git: `https://github.com/owner/repository`
|
||||
- Owner/repository format: `owner/repository`
|
||||
|
||||
#### Platform-Specific Behavior
|
||||
|
||||
**Linear Integration:** When GitHub integration is enabled for your Linear workspace with issue sync activated, the target repository is automatically detected from the linked GitHub issue. Manual specification is not required in this configuration.
|
||||
|
||||
**Jira Integrations:** Always include the repository information in your issue description or `@openhands` comment to ensure proper repository detection.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Platform Configuration Issues
|
||||
- **Webhook not triggering**: Verify the webhook URL is correct and the proper event types are selected (Comment, Issue updated)
|
||||
- **API authentication failing**: Check API key/token validity and ensure required scopes are granted
|
||||
- **Permission errors**: Ensure the service account has access to relevant projects/teams and appropriate permissions
|
||||
|
||||
### Workspace Integration Issues
|
||||
- **Workspace linking requests credentials**: If there are no active workspace integrations for the workspace you specified, you need to configure it first. Contact your platform administrator that you want to integrate with (eg: Jira, Linear)
|
||||
- **OAuth flow fails**: Ensure you're signing in with the same Git provider account that contains the repositories you want OpenHands to work on
|
||||
- **Integration not found**: Verify the workspace name matches exactly and that platform configuration was completed first
|
||||
|
||||
### General Issues
|
||||
- **Agent not responding**: Check webhook logs in your platform settings and verify service account status
|
||||
- **Authentication errors**: Verify Git provider permissions and OpenHands Cloud access
|
||||
- **Partial functionality**: Ensure both platform configuration and workspace integration are properly completed
|
||||
|
||||
### Getting Help
|
||||
For additional support, contact OpenHands Cloud support with:
|
||||
- Your integration platform (Linear, Jira Cloud, or Jira Data Center)
|
||||
- Workspace name
|
||||
- Error logs from webhook/integration attempts
|
||||
- Screenshots of configuration settings (without sensitive credentials)
|
||||
@@ -12,6 +12,10 @@ description: This guide walks you through installing the OpenHands Slack app.
|
||||
allowFullScreen>
|
||||
</iframe>
|
||||
|
||||
<Info>
|
||||
OpenHands utilizes a large language model (LLM), which may generate responses that are inaccurate or incomplete. While we strive for accuracy, OpenHands' outputs are not guaranteed to be correct, and we encourage users to validate critical information independently.
|
||||
</Info>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Access to OpenHands Cloud.
|
||||
|
||||
@@ -20,7 +20,7 @@ for scripting.
|
||||
|
||||
### Running with Python
|
||||
|
||||
**Note** - OpenHands requires Python version 3.12 or higher (Python 3.14 is not currently supported)
|
||||
**Note** - OpenHands requires Python version 3.12 or higher (Python 3.14 is not currently supported) and `uvx` for the default `fetch` MCP server (more details below).
|
||||
|
||||
1. Install OpenHands using pip:
|
||||
```bash
|
||||
@@ -103,7 +103,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -112,7 +112,7 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--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.50 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.51 \
|
||||
python -m openhands.cli.main --override-cli-mode true
|
||||
```
|
||||
|
||||
@@ -186,7 +186,7 @@ To configure Model Context Protocol (MCP) servers, you can refer to the document
|
||||
|
||||
This command modifies your `~/.openhands/config.toml` file and will prompt you to restart OpenHands for changes to take effect.
|
||||
|
||||
To enable the [Tavily MCP server](https://github.com/tavily-ai/tavily-mcp) search engine, you can set the `search_api_key` under the `[core]` section in the `~/.openhands/config.toml` file.
|
||||
By default, the [Fetch MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch) will be automatically configured for OpenHands. You can also [enable search engine](../search-engine-setup) via the [Tavily MCP server](https://github.com/tavily-ai/tavily-mcp) by setting the `search_api_key` under the `[core]` section in the `~/.openhands/config.toml` file.
|
||||
|
||||
##### Example of the `config.toml` file with MCP server configuration:
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
|
||||
# Run OpenHands
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -73,7 +73,7 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--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.50 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.51 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
|
||||
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.50
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.51
|
||||
```
|
||||
|
||||
2. Wait until the server is running (see log below):
|
||||
```
|
||||
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.50
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.51
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
|
||||
@@ -30,5 +30,6 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
|
||||
## Pricing
|
||||
|
||||
Pricing follows official API provider rates.
|
||||
[You can view model prices here.](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
|
||||
Pricing follows official API provider rates. [You can view model prices here.](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
|
||||
|
||||
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: $0.4 per million input tokens and $1.6 per million output tokens.
|
||||
|
||||
@@ -67,17 +67,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
### Start the App
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.50-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.50
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.51
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
@@ -120,6 +120,9 @@ describe("ExpandableMessage", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
const RouterStub = createRoutesStub([
|
||||
|
||||
@@ -28,7 +28,6 @@ describe("EventMessage", () => {
|
||||
action: "finish" as const,
|
||||
args: {
|
||||
final_thought: "Task completed successfully",
|
||||
task_completed: "success" as const,
|
||||
outputs: {},
|
||||
thought: "Task completed successfully",
|
||||
},
|
||||
@@ -114,7 +113,6 @@ describe("EventMessage", () => {
|
||||
action: "finish" as const,
|
||||
args: {
|
||||
final_thought: "Task completed successfully",
|
||||
task_completed: "success" as const,
|
||||
outputs: {},
|
||||
thought: "Task completed successfully",
|
||||
},
|
||||
|
||||
@@ -105,22 +105,12 @@ describe("MicroagentManagement", () => {
|
||||
const mockMicroagents: RepositoryMicroagent[] = [
|
||||
{
|
||||
name: "test-microagent-1",
|
||||
type: "repo",
|
||||
content: "Test microagent content 1",
|
||||
triggers: ["test", "microagent"],
|
||||
inputs: [],
|
||||
tools: [],
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/test-microagent-1",
|
||||
},
|
||||
{
|
||||
name: "test-microagent-2",
|
||||
type: "knowledge",
|
||||
content: "Test microagent content 2",
|
||||
triggers: ["knowledge", "test"],
|
||||
inputs: [],
|
||||
tools: [],
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/test-microagent-2",
|
||||
@@ -173,6 +163,13 @@ describe("MicroagentManagement", () => {
|
||||
vi.spyOn(OpenHands, "searchConversations").mockResolvedValue([
|
||||
...mockConversations,
|
||||
]);
|
||||
// Setup default mock for getRepositoryMicroagentContent
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Original microagent content for testing updates",
|
||||
path: ".openhands/microagents/update-test-microagent",
|
||||
git_provider: "github",
|
||||
triggers: ["test", "update"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the microagent management page", async () => {
|
||||
@@ -1187,17 +1184,6 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
expect(conversation1).toBeInTheDocument();
|
||||
expect(conversation2).toBeInTheDocument();
|
||||
|
||||
// Check that created dates are displayed for conversations (there are multiple elements with the same text)
|
||||
const createdDates = screen.getAllByText(
|
||||
/COMMON\$CREATED_ON.*10\/01\/2021/,
|
||||
);
|
||||
expect(createdDates.length).toBeGreaterThan(0);
|
||||
|
||||
const createdDates2 = screen.getAllByText(
|
||||
/COMMON\$CREATED_ON.*10\/02\/2021/,
|
||||
);
|
||||
expect(createdDates2.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should handle multiple repository expansions with conversations", async () => {
|
||||
@@ -1475,11 +1461,6 @@ describe("MicroagentManagement", () => {
|
||||
describe("MicroagentManagementMain", () => {
|
||||
const mockRepositoryMicroagent: RepositoryMicroagent = {
|
||||
name: "test-microagent",
|
||||
type: "repo",
|
||||
content: "Test microagent content",
|
||||
triggers: ["test", "microagent"],
|
||||
inputs: [],
|
||||
tools: [],
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/test-microagent",
|
||||
@@ -1820,11 +1801,6 @@ describe("MicroagentManagement", () => {
|
||||
it("should handle microagent with all required properties", async () => {
|
||||
const completeMicroagent: RepositoryMicroagent = {
|
||||
name: "complete-microagent",
|
||||
type: "knowledge",
|
||||
content: "Complete microagent content with all properties",
|
||||
triggers: ["complete", "test"],
|
||||
inputs: ["input1", "input2"],
|
||||
tools: ["tool1", "tool2"],
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/complete-microagent",
|
||||
@@ -1874,11 +1850,6 @@ describe("MicroagentManagement", () => {
|
||||
describe("Update microagent functionality", () => {
|
||||
const mockMicroagentForUpdate: RepositoryMicroagent = {
|
||||
name: "update-test-microagent",
|
||||
type: "repo",
|
||||
content: "Original microagent content for testing updates",
|
||||
triggers: ["original", "test"],
|
||||
inputs: [],
|
||||
tools: [],
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/update-test-microagent",
|
||||
@@ -1999,11 +1970,13 @@ describe("MicroagentManagement", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the form fields are populated with existing data
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue(
|
||||
"Original microagent content for testing updates",
|
||||
);
|
||||
// Wait for the content to be loaded and form fields to be populated
|
||||
await waitFor(() => {
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue(
|
||||
"Original microagent content for testing updates",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle update microagent form submission", async () => {
|
||||
@@ -2207,12 +2180,16 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle update modal with microagent that has no content", async () => {
|
||||
const user = userEvent.setup();
|
||||
const microagentWithoutContent = {
|
||||
...mockMicroagentForUpdate,
|
||||
content: "",
|
||||
};
|
||||
|
||||
// Render with update modal visible and microagent without content
|
||||
// Mock the content API to return empty content for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "",
|
||||
path: ".openhands/microagents/update-test-microagent",
|
||||
git_provider: "github",
|
||||
triggers: [],
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
@@ -2222,7 +2199,7 @@ describe("MicroagentManagement", () => {
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: microagentWithoutContent,
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
@@ -2243,19 +2220,25 @@ describe("MicroagentManagement", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the form field is empty
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue("");
|
||||
// Wait for the content to be loaded and check that the form field is empty
|
||||
await waitFor(() => {
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle update modal with microagent that has no triggers", async () => {
|
||||
const user = userEvent.setup();
|
||||
const microagentWithoutTriggers = {
|
||||
...mockMicroagentForUpdate,
|
||||
triggers: [],
|
||||
};
|
||||
|
||||
// Render with update modal visible and microagent without triggers
|
||||
// Mock the content API to return content without triggers for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Original microagent content for testing updates",
|
||||
path: ".openhands/microagents/update-test-microagent",
|
||||
git_provider: "github",
|
||||
triggers: [],
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
@@ -2265,7 +2248,7 @@ describe("MicroagentManagement", () => {
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: microagentWithoutTriggers,
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
@@ -2397,11 +2380,6 @@ describe("MicroagentManagement", () => {
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([
|
||||
{
|
||||
name: "test-microagent",
|
||||
type: "repo",
|
||||
content: "Test content",
|
||||
triggers: [],
|
||||
inputs: [],
|
||||
tools: [],
|
||||
created_at: "2021-10-01",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/test",
|
||||
@@ -2486,11 +2464,6 @@ describe("MicroagentManagement", () => {
|
||||
describe("Learn something new button functionality", () => {
|
||||
const mockMicroagentForLearn: RepositoryMicroagent = {
|
||||
name: "learn-test-microagent",
|
||||
type: "repo",
|
||||
content: "Test microagent content for learn functionality",
|
||||
triggers: ["learn", "test"],
|
||||
inputs: [],
|
||||
tools: [],
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/learn-test-microagent",
|
||||
@@ -2586,6 +2559,14 @@ describe("MicroagentManagement", () => {
|
||||
it("should populate form fields with current microagent data when learn button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock the content API to return the expected content for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Test microagent content for learn functionality",
|
||||
path: ".openhands/microagents/learn-test-microagent",
|
||||
git_provider: "github",
|
||||
triggers: ["learn", "test"],
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
@@ -2626,21 +2607,27 @@ describe("MicroagentManagement", () => {
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check that the form fields are populated with current microagent data
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue(
|
||||
"Test microagent content for learn functionality",
|
||||
);
|
||||
// Wait for the content to be loaded and form to be populated
|
||||
await waitFor(() => {
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue(
|
||||
"Test microagent content for learn functionality",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle learn button click with microagent that has no content", async () => {
|
||||
const user = userEvent.setup();
|
||||
const microagentWithoutContent = {
|
||||
...mockMicroagentForLearn,
|
||||
content: "",
|
||||
};
|
||||
|
||||
// Render with selected microagent without content
|
||||
// Mock the content API to return empty content for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "",
|
||||
path: ".openhands/microagents/learn-test-microagent",
|
||||
git_provider: "github",
|
||||
triggers: [],
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
@@ -2650,7 +2637,7 @@ describe("MicroagentManagement", () => {
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: microagentWithoutContent,
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
@@ -2680,19 +2667,25 @@ describe("MicroagentManagement", () => {
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check that the form field is empty
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue("");
|
||||
// Wait for the content to be loaded and check that the form field is empty
|
||||
await waitFor(() => {
|
||||
const queryInput = screen.getByTestId("query-input");
|
||||
expect(queryInput).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle learn button click with microagent that has no triggers", async () => {
|
||||
const user = userEvent.setup();
|
||||
const microagentWithoutTriggers = {
|
||||
...mockMicroagentForLearn,
|
||||
triggers: [],
|
||||
};
|
||||
|
||||
// Render with selected microagent without triggers
|
||||
// Mock the content API to return content without triggers for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Test microagent content for learn functionality",
|
||||
path: ".openhands/microagents/learn-test-microagent",
|
||||
git_provider: "github",
|
||||
triggers: [],
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
@@ -2702,7 +2695,7 @@ describe("MicroagentManagement", () => {
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: microagentWithoutTriggers,
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
|
||||
@@ -28,6 +28,9 @@ describe("PaymentForm", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,64 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, test, vi, afterEach } from "vitest";
|
||||
import { describe, expect, it, test, vi, afterEach, beforeEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { UserActions } from "#/components/features/sidebar/user-actions";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
// Create a mock for useIsAuthed that we can control per test
|
||||
const useIsAuthedMock = vi
|
||||
.fn()
|
||||
.mockReturnValue({ data: true, isLoading: false });
|
||||
|
||||
// Mock the useIsAuthed hook
|
||||
vi.mock("#/hooks/query/use-is-authed", () => ({
|
||||
useIsAuthed: () => useIsAuthedMock(),
|
||||
}));
|
||||
|
||||
describe("UserActions", () => {
|
||||
const user = userEvent.setup();
|
||||
const onClickAccountSettingsMock = vi.fn();
|
||||
const onLogoutMock = vi.fn();
|
||||
|
||||
// Create a wrapper with QueryClientProvider
|
||||
const renderWithQueryClient = (ui: ReactElement) => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return render(ui, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the mock to default value before each test
|
||||
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
onClickAccountSettingsMock.mockClear();
|
||||
onLogoutMock.mockClear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
expect(screen.getByTestId("user-actions")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle the user menu when the user avatar is clicked", async () => {
|
||||
render(
|
||||
renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
@@ -43,7 +80,7 @@ describe("UserActions", () => {
|
||||
});
|
||||
|
||||
it("should call onLogout and close the menu when the logout option is clicked", async () => {
|
||||
render(
|
||||
renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
@@ -62,20 +99,25 @@ describe("UserActions", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should NOT show context menu when user is undefined and avatar is clicked", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
it("should NOT show context menu when user is not authenticated and avatar is clicked", async () => {
|
||||
// Set isAuthed to false for this test
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
// Context menu should NOT appear because user is undefined
|
||||
// Context menu should NOT appear because user is not authenticated
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show context menu even when user has no avatar_url", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} user={{ avatar_url: "" }} />);
|
||||
renderWithQueryClient(
|
||||
<UserActions onLogout={onLogoutMock} user={{ avatar_url: "" }} />,
|
||||
);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
@@ -86,42 +128,74 @@ describe("UserActions", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should NOT be able to access logout when no user is provided", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
it("should NOT be able to access logout when user is not authenticated", async () => {
|
||||
// Set isAuthed to false for this test
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
// Logout option should not be accessible because context menu doesn't appear
|
||||
expect(
|
||||
screen.queryByText("ACCOUNT_SETTINGS$LOGOUT"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(onLogoutMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle user prop changing from undefined to defined", () => {
|
||||
const { rerender } = render(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
// Initially no user - context menu shouldn't work
|
||||
// Context menu should NOT appear because user is not authenticated
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Add user prop
|
||||
// Logout option should NOT be accessible when user is not authenticated
|
||||
expect(screen.queryByText("ACCOUNT_SETTINGS$LOGOUT")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle user prop changing from undefined to defined", async () => {
|
||||
// Start with no authentication
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
const { rerender } = renderWithQueryClient(
|
||||
<UserActions onLogout={onLogoutMock} />,
|
||||
);
|
||||
|
||||
// Initially no user and not authenticated - menu should not appear
|
||||
let userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Set authentication to true for the rerender
|
||||
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
|
||||
|
||||
// Add user prop and create a new QueryClient to ensure fresh state
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
rerender(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>,
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Component should still render correctly
|
||||
expect(screen.getByTestId("user-actions")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
|
||||
|
||||
// Menu should now work with user defined and authenticated
|
||||
userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle user prop changing from defined to undefined", async () => {
|
||||
const { rerender } = render(
|
||||
const { rerender } = renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
@@ -135,16 +209,27 @@ describe("UserActions", () => {
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Remove user prop - menu should disappear
|
||||
rerender(<UserActions onLogout={onLogoutMock} />);
|
||||
// Set authentication to false for the rerender
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
// Remove user prop - menu should disappear because user is no longer authenticated
|
||||
rerender(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<UserActions onLogout={onLogoutMock} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Context menu should NOT be visible when user becomes unauthenticated
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Logout option should not be accessible
|
||||
expect(screen.queryByText("ACCOUNT_SETTINGS$LOGOUT")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should work with loading state and user provided", async () => {
|
||||
render(
|
||||
renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useFeedbackExists } from "#/hooks/query/use-feedback-exists";
|
||||
|
||||
// Mock the useConfig hook
|
||||
vi.mock("#/hooks/query/use-config", () => ({
|
||||
useConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the useConversationId hook
|
||||
vi.mock("#/hooks/use-conversation-id", () => ({
|
||||
useConversationId: () => ({ conversationId: "test-conversation-id" }),
|
||||
}));
|
||||
|
||||
describe("useFeedbackExists", () => {
|
||||
let queryClient: QueryClient;
|
||||
const mockCheckFeedbackExists = vi.spyOn(OpenHands, "checkFeedbackExists");
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
mockCheckFeedbackExists.mockClear();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
it("should not call API when APP_MODE is not saas", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: { APP_MODE: "oss" },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(123), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for any potential async operations
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was not called
|
||||
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
|
||||
|
||||
// Verify that the query is disabled
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should call API when APP_MODE is saas", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: { APP_MODE: "saas" },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
mockCheckFeedbackExists.mockResolvedValue({
|
||||
exists: true,
|
||||
rating: 5,
|
||||
reason: "Great job!",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(123), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for the query to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was called
|
||||
expect(mockCheckFeedbackExists).toHaveBeenCalledWith(
|
||||
"test-conversation-id",
|
||||
123,
|
||||
);
|
||||
|
||||
// Verify that the data is returned
|
||||
expect(result.current.data).toEqual({
|
||||
exists: true,
|
||||
rating: 5,
|
||||
reason: "Great job!",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call API when eventId is not provided", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: { APP_MODE: "saas" },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(undefined), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for any potential async operations
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was not called
|
||||
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
|
||||
|
||||
// Verify that the query is disabled
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not call API when config is not loaded yet", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(123), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for any potential async operations
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was not called
|
||||
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
|
||||
|
||||
// Verify that the query is disabled
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -76,6 +76,9 @@ describe("frontend/routes/_oh", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,6 +114,9 @@ describe("frontend/routes/_oh", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -192,6 +198,9 @@ describe("frontend/routes/_oh", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ const VALID_OSS_CONFIG: GetConfigResponse = {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,6 +30,9 @@ const VALID_SAAS_CONFIG: GetConfigResponse = {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -333,6 +333,9 @@ describe("Settings 404", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
@@ -355,6 +358,9 @@ describe("Setup Payment modal", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
|
||||
@@ -101,7 +101,8 @@ describe("Content", () => {
|
||||
|
||||
renderSecretsSettings();
|
||||
|
||||
expect(getSecretsSpy).not.toHaveBeenCalled();
|
||||
// In SAAS mode, getSecrets is still called because the user is authenticated
|
||||
await waitFor(() => expect(getSecretsSpy).toHaveBeenCalled());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
@@ -86,6 +86,9 @@ describe("Settings Billing", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -104,6 +107,9 @@ describe("Settings Billing", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,6 +128,9 @@ describe("Settings Billing", () => {
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
469
frontend/package-lock.json
generated
469
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.50.0",
|
||||
"version": "0.51.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.50.0",
|
||||
"version": "0.51.1",
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.2",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
@@ -19,22 +19,22 @@
|
||||
"@stripe/stripe-js": "^7.7.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query": "^5.84.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.11.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.11",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.29",
|
||||
"jose": "^6.0.12",
|
||||
"lucide-react": "^0.533.0",
|
||||
"lucide-react": "^0.536.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.258.2",
|
||||
"posthog-js": "^1.258.5",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vite": "^7.0.6",
|
||||
"web-vitals": "^5.0.3",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,10 +60,10 @@
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@react-router/dev": "^7.7.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.81.2",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -95,9 +95,9 @@
|
||||
"lint-staged": "^16.1.2",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.3.0",
|
||||
"stripe": "^18.4.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.2"
|
||||
@@ -3407,12 +3407,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.54.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz",
|
||||
"integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==",
|
||||
"version": "1.54.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
|
||||
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.54.1"
|
||||
"playwright": "1.54.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -5830,13 +5830,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query": {
|
||||
"version": "5.81.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.81.2.tgz",
|
||||
"integrity": "sha512-h4k6P6fm5VhKP5NkK+0TTVpGGyKQdx6tk7NYYG7J7PkSu7ClpLgBihw7yzK8N3n5zPaF3IMyErxfoNiXWH/3/A==",
|
||||
"version": "5.83.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.83.1.tgz",
|
||||
"integrity": "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.18.1"
|
||||
"@typescript-eslint/utils": "^8.37.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5846,21 +5845,194 @@
|
||||
"eslint": "^8.57.0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
|
||||
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.38.0",
|
||||
"@typescript-eslint/types": "^8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
|
||||
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
|
||||
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
|
||||
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.38.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
|
||||
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
|
||||
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
|
||||
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
|
||||
"version": "5.83.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
|
||||
"integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
|
||||
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
|
||||
"version": "5.84.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz",
|
||||
"integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.83.0"
|
||||
"@tanstack/query-core": "5.83.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6290,42 +6462,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
||||
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
||||
"@typescript-eslint/types": "^8.36.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
||||
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
|
||||
@@ -6344,23 +6480,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
||||
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
|
||||
@@ -6455,135 +6574,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
||||
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/typescript-estree": "8.36.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
||||
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
||||
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
||||
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.36.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
||||
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
|
||||
@@ -9890,11 +9880,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.11",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.11.tgz",
|
||||
"integrity": "sha512-VzNi+exyI3bn7Pzvz1Fjap1VO9gQu8mxrsSsNamMidsZ8AA8W2kQsR+YQOciEUbMtkKAWIbPHPttfn5e9jqqJQ==",
|
||||
"version": "12.23.12",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
|
||||
"integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.9",
|
||||
"motion-dom": "^12.23.12",
|
||||
"motion-utils": "^12.23.6",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
@@ -12207,9 +12197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.533.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.533.0.tgz",
|
||||
"integrity": "sha512-XwRo6CQowPRe1cfBJITmHytjR3XS4AZpV6rrBFEzoghARgyU2RK3yNRSnRkSFFSQJWFdQ8f4Wk1awgLLSy1NCQ==",
|
||||
"version": "0.536.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.536.0.tgz",
|
||||
"integrity": "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
@@ -13363,9 +13353,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.9",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.9.tgz",
|
||||
"integrity": "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==",
|
||||
"version": "12.23.12",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
|
||||
"integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.23.6"
|
||||
}
|
||||
@@ -14128,12 +14118,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.54.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz",
|
||||
"integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==",
|
||||
"version": "1.54.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
|
||||
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.54.1"
|
||||
"playwright-core": "1.54.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -14146,9 +14136,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.54.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz",
|
||||
"integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==",
|
||||
"version": "1.54.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
|
||||
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
@@ -14217,9 +14207,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.258.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.258.2.tgz",
|
||||
"integrity": "sha512-XBSeiN4HjiYsy3tW5zss8WOJF2JXTQXAYw2wZ+zjqQuzzi7kkLEXjIgsVrBnt5Opwhqn0krZVsb0ZBw34dIiyQ==",
|
||||
"version": "1.258.5",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.258.5.tgz",
|
||||
"integrity": "sha512-Tx6CzS8MsGAQGPrQth5TbkGxGQgAY01SktNW773/KDmVOWiRVZq/WQF/MRJRiuFxJ7qjethZQi3aBWfWKdr1RA==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
@@ -16235,11 +16225,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-18.3.0.tgz",
|
||||
"integrity": "sha512-FkxrTUUcWB4CVN2yzgsfF/YHD6WgYHduaa7VmokCy5TLCgl5UNJkwortxcedrxSavQ8Qfa4Ir4JxcbIYiBsyLg==",
|
||||
"version": "18.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-18.4.0.tgz",
|
||||
"integrity": "sha512-LKFeDnDYo4U/YzNgx2Lc9PT9XgKN0JNF1iQwZxgkS4lOw5NunWCnzyH5RhTlD3clIZnf54h7nyMWkS8VXPmtTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"qs": "^6.11.0"
|
||||
},
|
||||
@@ -16794,11 +16783,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -17424,10 +17412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.0.3.tgz",
|
||||
"integrity": "sha512-4KmOFYxj7qT6RAdCH0SWwq8eKeXNhAFXR4PmgF6nrWFmrJ41n7lq3UCA6UK0GebQ4uu+XP8e8zGjaDO3wZlqTg==",
|
||||
"license": "Apache-2.0"
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
|
||||
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.50.0",
|
||||
"version": "0.51.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -18,22 +18,22 @@
|
||||
"@stripe/stripe-js": "^7.7.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query": "^5.84.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.11.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.11",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.29",
|
||||
"jose": "^6.0.12",
|
||||
"lucide-react": "^0.533.0",
|
||||
"lucide-react": "^0.536.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.258.2",
|
||||
"posthog-js": "^1.258.5",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -51,7 +51,7 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vite": "^7.0.6",
|
||||
"web-vitals": "^5.0.3",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -84,10 +84,10 @@
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@react-router/dev": "^7.7.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.81.2",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -119,9 +119,9 @@
|
||||
"lint-staged": "^16.1.2",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.3.0",
|
||||
"stripe": "^18.4.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.2"
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
GetMicroagentsResponse,
|
||||
GetMicroagentPromptResponse,
|
||||
CreateMicroagent,
|
||||
MicroagentContentResponse,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
|
||||
import { GitUser, GitRepository, Branch } from "#/types/git";
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { BatchFeedbackData } from "#/hooks/query/use-batch-feedback";
|
||||
|
||||
class OpenHands {
|
||||
private static currentConversation: Conversation | null = null;
|
||||
@@ -166,6 +168,38 @@ class OpenHands {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feedback for multiple events in a conversation
|
||||
* @param conversationId The conversation ID
|
||||
* @returns Map of event IDs to feedback data including existence, rating, reason and metadata
|
||||
*/
|
||||
static async getBatchFeedback(conversationId: string): Promise<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
metadata?: Record<string, BatchFeedbackData>;
|
||||
}
|
||||
>
|
||||
> {
|
||||
const url = `/feedback/conversation/${conversationId}/batch`;
|
||||
const { data } = await openHands.get<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
metadata?: Record<string, BatchFeedbackData>;
|
||||
}
|
||||
>
|
||||
>(url);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with GitHub token
|
||||
* @returns Response with authentication status and user info if successful
|
||||
@@ -491,7 +525,7 @@ class OpenHands {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available microagents for a specific repository
|
||||
* Get the available microagents for a repository
|
||||
* @param owner The repository owner
|
||||
* @param repo The repository name
|
||||
* @returns The available microagents for the repository
|
||||
@@ -506,6 +540,27 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of a specific microagent from a repository
|
||||
* @param owner The repository owner
|
||||
* @param repo The repository name
|
||||
* @param filePath The path to the microagent file within the repository
|
||||
* @returns The microagent content and metadata
|
||||
*/
|
||||
static async getRepositoryMicroagentContent(
|
||||
owner: string,
|
||||
repo: string,
|
||||
filePath: string,
|
||||
): Promise<MicroagentContentResponse> {
|
||||
const { data } = await openHands.get<MicroagentContentResponse>(
|
||||
`/api/user/repository/${owner}/${repo}/microagents/content`,
|
||||
{
|
||||
params: { file_path: filePath },
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getMicroagentPrompt(
|
||||
conversationId: string,
|
||||
eventId: number,
|
||||
|
||||
@@ -55,6 +55,9 @@ export interface GetConfigResponse {
|
||||
ENABLE_BILLING: boolean;
|
||||
HIDE_LLM_SETTINGS: boolean;
|
||||
HIDE_MICROAGENT_MANAGEMENT?: boolean;
|
||||
ENABLE_JIRA: boolean;
|
||||
ENABLE_JIRA_DC: boolean;
|
||||
ENABLE_LINEAR: boolean;
|
||||
};
|
||||
MAINTENANCE?: {
|
||||
startTime: string;
|
||||
@@ -147,3 +150,10 @@ export interface CreateMicroagent {
|
||||
git_provider?: Provider;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface MicroagentContentResponse {
|
||||
content: string;
|
||||
path: string;
|
||||
git_provider: Provider;
|
||||
triggers: string[];
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ActionSuggestions({
|
||||
pr,
|
||||
prShort,
|
||||
pushToBranch: `Please push the changes to a remote branch on ${getProviderName()}, but do NOT create a ${pr}. Check your current branch name first - if it's main, master, deploy, or another common default branch name, create a new branch with a descriptive name related to your changes. Otherwise, use the exact SAME branch name as the one you are currently on.`,
|
||||
createPR: `Please push the changes to ${getProviderName()} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
|
||||
createPR: `Please push the changes to ${getProviderName()} and open a ${pr}. If you're on a default branch (e.g., main, master, deploy), create a new branch with a descriptive name otherwise use the current branch. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
|
||||
pushToPR: `Please push the latest changes to the existing ${pr}.`,
|
||||
};
|
||||
|
||||
|
||||
@@ -77,25 +77,8 @@ const getMcpActionContent = (event: MCPAction): string => {
|
||||
const getThinkActionContent = (event: ThinkAction): string =>
|
||||
event.args.thought;
|
||||
|
||||
const getFinishActionContent = (event: FinishAction): string => {
|
||||
let content = event.args.final_thought;
|
||||
|
||||
switch (event.args.task_completed) {
|
||||
case "success":
|
||||
content += `\n\n\n${i18n.t("FINISH$TASK_COMPLETED_SUCCESSFULLY")}`;
|
||||
break;
|
||||
case "failure":
|
||||
content += `\n\n\n${i18n.t("FINISH$TASK_NOT_COMPLETED")}`;
|
||||
break;
|
||||
case "partial":
|
||||
default:
|
||||
content += `\n\n\n${i18n.t("FINISH$TASK_COMPLETED_PARTIALLY")}`;
|
||||
break;
|
||||
}
|
||||
|
||||
return content.trim();
|
||||
};
|
||||
|
||||
const getFinishActionContent = (event: FinishAction): string =>
|
||||
event.args.final_thought.trim();
|
||||
const getNoContentActionContent = (): string => "";
|
||||
|
||||
export const getActionContent = (event: OpenHandsAction): string => {
|
||||
|
||||
@@ -22,7 +22,7 @@ export function AccountSettingsContextMenu({
|
||||
ref={ref}
|
||||
className="absolute right-full md:left-full -top-1 z-10 w-fit"
|
||||
>
|
||||
<ContextMenuListItem onClick={onLogout}>
|
||||
<ContextMenuListItem onClick={onLogout} data-testid="logout-button">
|
||||
{t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)}
|
||||
</ContextMenuListItem>
|
||||
</ContextMenu>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ContextMenuIconTextProps {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
text: string;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
export function ContextMenuIconText({
|
||||
icon: Icon,
|
||||
text,
|
||||
className,
|
||||
iconClassName,
|
||||
}: ContextMenuIconTextProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-3 px-1", className)}>
|
||||
<Icon className={cn("w-4 h-4 shrink-0", iconClassName)} />
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function ContextMenuListItem({
|
||||
onClick={onClick}
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"text-sm px-4 py-2 w-full text-start hover:bg-white/10 first-of-type:rounded-t-md last-of-type:rounded-b-md",
|
||||
"text-sm px-4 h-10 w-full text-start hover:bg-white/10 cursor-pointer",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent text-nowrap",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ContextMenu({
|
||||
<ul
|
||||
data-testid={testId}
|
||||
ref={ref}
|
||||
className={cn("bg-tertiary rounded-md", className)}
|
||||
className={cn("bg-tertiary rounded-md overflow-hidden", className)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import {
|
||||
Trash,
|
||||
Power,
|
||||
Pencil,
|
||||
Download,
|
||||
Wallet,
|
||||
Wrench,
|
||||
Bot,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ContextMenu } from "../context-menu/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ContextMenuSeparator } from "../context-menu/context-menu-separator";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { ContextMenuIconText } from "../context-menu/context-menu-icon-text";
|
||||
|
||||
interface ConversationCardContextMenuProps {
|
||||
onClose: () => void;
|
||||
@@ -31,6 +42,12 @@ export function ConversationCardContextMenu({
|
||||
const { t } = useTranslation();
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
|
||||
const hasEdit = Boolean(onEdit);
|
||||
const hasDownload = Boolean(onDownloadViaVSCode);
|
||||
const hasTools = Boolean(onShowAgentTools || onShowMicroagents);
|
||||
const hasInfo = Boolean(onDisplayCost);
|
||||
const hasControl = Boolean(onStop || onDelete);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
ref={ref}
|
||||
@@ -41,51 +58,84 @@ export function ConversationCardContextMenu({
|
||||
position === "bottom" && "top-full",
|
||||
)}
|
||||
>
|
||||
{onDelete && (
|
||||
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
|
||||
{t(I18nKey.BUTTON$DELETE)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onStop && (
|
||||
<ContextMenuListItem testId="stop-button" onClick={onStop}>
|
||||
{t(I18nKey.BUTTON$STOP)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onEdit && (
|
||||
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
|
||||
{t(I18nKey.BUTTON$EDIT_TITLE)}
|
||||
<ContextMenuIconText
|
||||
icon={Pencil}
|
||||
text={t(I18nKey.BUTTON$EDIT_TITLE)}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{hasEdit && (hasDownload || hasTools || hasInfo || hasControl) && (
|
||||
<ContextMenuSeparator />
|
||||
)}
|
||||
|
||||
{onDownloadViaVSCode && (
|
||||
<ContextMenuListItem
|
||||
testId="download-vscode-button"
|
||||
onClick={onDownloadViaVSCode}
|
||||
>
|
||||
{t(I18nKey.BUTTON$DOWNLOAD_VIA_VSCODE)}
|
||||
<ContextMenuIconText
|
||||
icon={Download}
|
||||
text={t(I18nKey.BUTTON$DOWNLOAD_VIA_VSCODE)}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onDisplayCost && (
|
||||
<ContextMenuListItem
|
||||
testId="display-cost-button"
|
||||
onClick={onDisplayCost}
|
||||
>
|
||||
{t(I18nKey.BUTTON$DISPLAY_COST)}
|
||||
</ContextMenuListItem>
|
||||
|
||||
{hasDownload && (hasTools || hasInfo || hasControl) && (
|
||||
<ContextMenuSeparator />
|
||||
)}
|
||||
|
||||
{onShowAgentTools && (
|
||||
<ContextMenuListItem
|
||||
testId="show-agent-tools-button"
|
||||
onClick={onShowAgentTools}
|
||||
>
|
||||
{t(I18nKey.BUTTON$SHOW_AGENT_TOOLS_AND_METADATA)}
|
||||
<ContextMenuIconText
|
||||
icon={Wrench}
|
||||
text={t(I18nKey.BUTTON$SHOW_AGENT_TOOLS_AND_METADATA)}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{onShowMicroagents && (
|
||||
<ContextMenuListItem
|
||||
testId="show-microagents-button"
|
||||
onClick={onShowMicroagents}
|
||||
>
|
||||
{t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
|
||||
<ContextMenuIconText
|
||||
icon={Bot}
|
||||
text={t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{hasTools && (hasInfo || hasControl) && <ContextMenuSeparator />}
|
||||
|
||||
{onDisplayCost && (
|
||||
<ContextMenuListItem
|
||||
testId="display-cost-button"
|
||||
onClick={onDisplayCost}
|
||||
>
|
||||
<ContextMenuIconText
|
||||
icon={Wallet}
|
||||
text={t(I18nKey.BUTTON$DISPLAY_COST)}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{hasInfo && hasControl && <ContextMenuSeparator />}
|
||||
|
||||
{onStop && (
|
||||
<ContextMenuListItem testId="stop-button" onClick={onStop}>
|
||||
<ContextMenuIconText icon={Power} text={t(I18nKey.BUTTON$STOP)} />
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
|
||||
<ContextMenuIconText icon={Trash} text={t(I18nKey.BUTTON$DELETE)} />
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
</ContextMenu>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { formatDateMMDDYYYY } from "#/utils/format-time-delta";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import {
|
||||
@@ -38,22 +37,6 @@ export function MicroagentManagementMicroagentCard({
|
||||
pr_number: prNumber,
|
||||
} = conversation ?? {};
|
||||
|
||||
// Format the repository URL to point to the microagent file
|
||||
const microagentFilePath = microagent
|
||||
? `.openhands/microagents/${microagent.name}`
|
||||
: "";
|
||||
|
||||
// Format the createdAt date using MM/DD/YYYY format
|
||||
const formattedCreatedAt = useMemo(() => {
|
||||
if (microagent) {
|
||||
return formatDateMMDDYYYY(new Date(microagent.created_at));
|
||||
}
|
||||
if (conversation) {
|
||||
return formatDateMMDDYYYY(new Date(conversation.created_at));
|
||||
}
|
||||
return "";
|
||||
}, [microagent, conversation]);
|
||||
|
||||
const hasPr = !!(prNumber && prNumber.length > 0);
|
||||
|
||||
// Helper function to get status text
|
||||
@@ -131,12 +114,9 @@ export function MicroagentManagementMicroagentCard({
|
||||
<div className="text-white text-[16px] font-semibold">{cardTitle}</div>
|
||||
{!!microagent && (
|
||||
<div className="text-white text-sm font-normal">
|
||||
{microagentFilePath}
|
||||
{microagent.path}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-white text-sm font-normal">
|
||||
{t(I18nKey.COMMON$CREATED_ON)} {formattedCreatedAt}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,12 @@ import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RootState } from "#/store";
|
||||
import XIcon from "#/icons/x.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { cn, extractRepositoryInfo } from "#/utils/utils";
|
||||
import { BadgeInput } from "#/components/shared/inputs/badge-input";
|
||||
import { MicroagentFormData } from "#/types/microagent-management";
|
||||
import { Branch, GitRepository } from "#/types/git";
|
||||
import { useRepositoryBranches } from "#/hooks/query/use-repository-branches";
|
||||
import { useRepositoryMicroagentContent } from "#/hooks/query/use-repository-microagent-content";
|
||||
import {
|
||||
BranchDropdown,
|
||||
BranchLoadingState,
|
||||
@@ -51,13 +52,23 @@ export function MicroagentManagementUpsertMicroagentModal({
|
||||
// Add a ref to track if the branch was manually cleared by the user
|
||||
const branchManuallyClearedRef = useRef<boolean>(false);
|
||||
|
||||
// Extract owner and repo from full_name for content API
|
||||
const { owner, repo, filePath } = extractRepositoryInfo(
|
||||
selectedRepository,
|
||||
microagent,
|
||||
);
|
||||
|
||||
// Fetch microagent content when updating
|
||||
const { data: microagentContentData, isLoading: isLoadingContent } =
|
||||
useRepositoryMicroagentContent(owner, repo, filePath, true);
|
||||
|
||||
// Populate form fields with existing microagent data when updating
|
||||
useEffect(() => {
|
||||
if (isUpdate && microagent) {
|
||||
setQuery(microagent.content);
|
||||
setTriggers(microagent.triggers || []);
|
||||
if (isUpdate && microagentContentData) {
|
||||
setQuery(microagentContentData.content);
|
||||
setTriggers(microagentContentData.triggers || []);
|
||||
}
|
||||
}, [isUpdate, microagent]);
|
||||
}, [isUpdate, microagentContentData]);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
@@ -294,10 +305,11 @@ export function MicroagentManagementUpsertMicroagentModal({
|
||||
isLoading ||
|
||||
isLoadingBranches ||
|
||||
!selectedBranch ||
|
||||
isBranchesError
|
||||
isBranchesError ||
|
||||
(isUpdate && isLoadingContent) // Disable while loading content for updates
|
||||
}
|
||||
>
|
||||
{isLoading || isLoadingBranches
|
||||
{isLoading || isLoadingBranches || (isUpdate && isLoadingContent)
|
||||
? t(I18nKey.HOME$LOADING)
|
||||
: t(I18nKey.MICROAGENT$LAUNCH)}
|
||||
</BrandButton>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@@ -7,8 +9,12 @@ import { ul, ol } from "../markdown/list";
|
||||
import { paragraph } from "../markdown/paragraph";
|
||||
import { anchor } from "../markdown/anchor";
|
||||
import { RootState } from "#/store";
|
||||
import { useRepositoryMicroagentContent } from "#/hooks/query/use-repository-microagent-content";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { extractRepositoryInfo } from "#/utils/utils";
|
||||
|
||||
export function MicroagentManagementViewMicroagentContent() {
|
||||
const { t } = useTranslation();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
@@ -19,55 +25,49 @@ export function MicroagentManagementViewMicroagentContent() {
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
const transformMicroagentContent = (): string => {
|
||||
if (!microagent) {
|
||||
return "";
|
||||
}
|
||||
// Extract owner and repo from full_name (e.g., "owner/repo")
|
||||
const { owner, repo, filePath } = extractRepositoryInfo(
|
||||
selectedRepository,
|
||||
microagent,
|
||||
);
|
||||
|
||||
// If no triggers exist, return the content as-is
|
||||
if (!microagent.triggers || microagent.triggers.length === 0) {
|
||||
return microagent.content;
|
||||
}
|
||||
|
||||
// Create the triggers frontmatter
|
||||
const triggersFrontmatter = `
|
||||
---
|
||||
|
||||
triggers:
|
||||
${microagent.triggers.map((trigger) => ` - ${trigger}`).join("\n")}
|
||||
|
||||
---
|
||||
`;
|
||||
|
||||
// Prepend the frontmatter to the content
|
||||
return `
|
||||
${triggersFrontmatter}
|
||||
|
||||
${microagent.content}
|
||||
`;
|
||||
};
|
||||
// Fetch microagent content using the new API
|
||||
const {
|
||||
data: microagentData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useRepositoryMicroagentContent(owner, repo, filePath, true);
|
||||
|
||||
if (!microagent || !selectedRepository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Transform the content to include triggers frontmatter if applicable
|
||||
const transformedContent = transformMicroagentContent();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full p-6 bg-[#ffffff1a] rounded-2xl text-white text-sm">
|
||||
<Markdown
|
||||
components={{
|
||||
code,
|
||||
ul,
|
||||
ol,
|
||||
a: anchor,
|
||||
p: paragraph,
|
||||
}}
|
||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||
>
|
||||
{transformedContent}
|
||||
</Markdown>
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Spinner size="lg" data-testid="loading-microagent-content-spinner" />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
{t(I18nKey.MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT)}
|
||||
</div>
|
||||
)}
|
||||
{microagentData && !isLoading && !error && (
|
||||
<Markdown
|
||||
components={{
|
||||
code,
|
||||
ul,
|
||||
ol,
|
||||
a: anchor,
|
||||
p: paragraph,
|
||||
}}
|
||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||
>
|
||||
{microagentData.content}
|
||||
</Markdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,16 +12,21 @@ export function ConfigureGitHubRepositoriesAnchor({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<a
|
||||
data-testid="configure-github-repositories-button"
|
||||
href={`https://github.com/apps/${slug}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="py-9"
|
||||
>
|
||||
<BrandButton type="button" variant="secondary">
|
||||
<div data-testid="configure-github-repositories-button" className="py-9">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="w-55"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`https://github.com/apps/${slug}/installations/new`,
|
||||
"_blank",
|
||||
"noreferrer noopener",
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(I18nKey.GITHUB$CONFIGURE_REPOS)}
|
||||
</BrandButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,21 @@ export function InstallSlackAppAnchor() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<a
|
||||
data-testid="install-slack-app-button"
|
||||
href="https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,channels:history,chat:write,groups:history,im:history,mpim:history,users:read&user_scope="
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="py-9"
|
||||
>
|
||||
<BrandButton type="button" variant="secondary">
|
||||
<div data-testid="install-slack-app-button" className="py-9">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="w-55"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,channels:history,chat:write,groups:history,im:history,mpim:history,users:read&user_scope=",
|
||||
"_blank",
|
||||
"noreferrer noopener",
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(I18nKey.SLACK$INSTALL_APP)}
|
||||
</BrandButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
import React, { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import {
|
||||
BaseModalDescription,
|
||||
BaseModalTitle,
|
||||
} from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
|
||||
import { useValidateIntegration } from "#/hooks/mutation/use-validate-integration";
|
||||
|
||||
interface ConfigureButtonProps {
|
||||
onClick: () => void;
|
||||
isDisabled: boolean;
|
||||
text?: string;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
export function ConfigureButton({
|
||||
onClick,
|
||||
isDisabled,
|
||||
text,
|
||||
"data-testid": dataTestId,
|
||||
}: ConfigureButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<BrandButton
|
||||
data-testid={dataTestId}
|
||||
variant="primary"
|
||||
onClick={onClick}
|
||||
isDisabled={isDisabled}
|
||||
type="button"
|
||||
className="w-30 min-w-20"
|
||||
>
|
||||
{text || t(I18nKey.PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL)}
|
||||
</BrandButton>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConfigureModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (data: {
|
||||
workspace: string;
|
||||
webhookSecret: string;
|
||||
serviceAccountEmail: string;
|
||||
serviceAccountApiKey: string;
|
||||
isActive: boolean;
|
||||
}) => void;
|
||||
onLink: (workspace: string) => void;
|
||||
onUnlink?: () => void;
|
||||
platformName: string;
|
||||
platform: "jira" | "jira-dc" | "linear";
|
||||
integrationData?: {
|
||||
id: number;
|
||||
keycloak_user_id: string;
|
||||
status: string;
|
||||
workspace?: {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
editable: boolean;
|
||||
};
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function ConfigureModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
onLink,
|
||||
onUnlink,
|
||||
platformName,
|
||||
platform,
|
||||
integrationData,
|
||||
}: ConfigureModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useState("");
|
||||
const [webhookSecret, setWebhookSecret] = useState("");
|
||||
const [serviceAccountEmail, setServiceAccountEmail] = useState("");
|
||||
const [serviceAccountApiKey, setServiceAccountApiKey] = useState("");
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [showConfigurationFields, setShowConfigurationFields] = useState(false);
|
||||
|
||||
// Determine initial state based on integrationData
|
||||
const existingWorkspace = integrationData?.workspace;
|
||||
const isWorkspaceEditable = existingWorkspace?.editable ?? false;
|
||||
|
||||
// Set initial workspace value when modal opens
|
||||
React.useEffect(() => {
|
||||
if (isOpen && existingWorkspace) {
|
||||
setWorkspace(existingWorkspace.name);
|
||||
setShowConfigurationFields(isWorkspaceEditable);
|
||||
} else if (isOpen && !existingWorkspace) {
|
||||
setWorkspace("");
|
||||
setShowConfigurationFields(false);
|
||||
}
|
||||
}, [isOpen, existingWorkspace, isWorkspaceEditable]);
|
||||
|
||||
// Validation states
|
||||
const [workspaceError, setWorkspaceError] = useState<string | null>(null);
|
||||
const [webhookSecretError, setWebhookSecretError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [emailError, setEmailError] = useState<string | null>(null);
|
||||
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
|
||||
|
||||
const validateMutation = useValidateIntegration(platform, {
|
||||
onSuccess: (data) => {
|
||||
if (data.data.status === "active") {
|
||||
// Validation successful, proceed with linking
|
||||
onLink(workspace.trim());
|
||||
} else {
|
||||
// Show configuration fields for further setup
|
||||
setShowConfigurationFields(true);
|
||||
setIsActive(true);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error.response?.status === 404) {
|
||||
// Integration not found, show configuration fields
|
||||
setShowConfigurationFields(true);
|
||||
setIsActive(true);
|
||||
} else {
|
||||
// Other errors - still show configuration fields as fallback
|
||||
setShowConfigurationFields(true);
|
||||
setIsActive(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Validation functions
|
||||
const validateWorkspace = (value: string) => {
|
||||
const isValid = /^[a-zA-Z0-9\-_.]*$/.test(value);
|
||||
if (!isValid && value.length > 0) {
|
||||
setWorkspaceError(
|
||||
t(I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR),
|
||||
);
|
||||
} else {
|
||||
setWorkspaceError(null);
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const validateWebhookSecret = (value: string) => {
|
||||
const hasSpaces = /\s/.test(value);
|
||||
if (hasSpaces) {
|
||||
setWebhookSecretError(
|
||||
t(I18nKey.PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR),
|
||||
);
|
||||
} else {
|
||||
setWebhookSecretError(null);
|
||||
}
|
||||
return !hasSpaces;
|
||||
};
|
||||
|
||||
const validateEmail = (value: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const isValid = emailRegex.test(value) || value.length === 0;
|
||||
if (!isValid && value.length > 0) {
|
||||
setEmailError(
|
||||
t(I18nKey.PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR),
|
||||
);
|
||||
} else {
|
||||
setEmailError(null);
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const validateApiKey = (value: string) => {
|
||||
const hasSpaces = /\s/.test(value);
|
||||
if (hasSpaces) {
|
||||
setApiKeyError(
|
||||
t(I18nKey.PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR),
|
||||
);
|
||||
} else {
|
||||
setApiKeyError(null);
|
||||
}
|
||||
return !hasSpaces;
|
||||
};
|
||||
|
||||
// Input handlers with validation
|
||||
const handleWorkspaceChange = (value: string) => {
|
||||
setWorkspace(value);
|
||||
validateWorkspace(value);
|
||||
};
|
||||
|
||||
const handleWebhookSecretChange = (value: string) => {
|
||||
setWebhookSecret(value);
|
||||
validateWebhookSecret(value);
|
||||
};
|
||||
|
||||
const handleEmailChange = (value: string) => {
|
||||
setServiceAccountEmail(value);
|
||||
validateEmail(value);
|
||||
};
|
||||
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
setServiceAccountApiKey(value);
|
||||
validateApiKey(value);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setWorkspace("");
|
||||
setWebhookSecret("");
|
||||
setServiceAccountEmail("");
|
||||
setServiceAccountApiKey("");
|
||||
setIsActive(false);
|
||||
setShowConfigurationFields(false);
|
||||
setWorkspaceError(null);
|
||||
setWebhookSecretError(null);
|
||||
setEmailError(null);
|
||||
setApiKeyError(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleConnect = () => {
|
||||
if (showConfigurationFields) {
|
||||
// Full configuration flow (either new configuration or editing existing)
|
||||
onConfirm({
|
||||
workspace,
|
||||
webhookSecret,
|
||||
serviceAccountEmail,
|
||||
serviceAccountApiKey,
|
||||
isActive,
|
||||
});
|
||||
} else if (!existingWorkspace) {
|
||||
// First check the workspace with validation for new integrations
|
||||
validateMutation.mutate(workspace.trim());
|
||||
}
|
||||
// For existing workspace that's not editable, no action needed
|
||||
// This case shouldn't happen as the button should be hidden
|
||||
};
|
||||
|
||||
const isConnectDisabled = showConfigurationFields
|
||||
? !workspace.trim() ||
|
||||
!webhookSecret.trim() ||
|
||||
!serviceAccountEmail.trim() ||
|
||||
!serviceAccountApiKey.trim() ||
|
||||
workspaceError !== null ||
|
||||
webhookSecretError !== null ||
|
||||
emailError !== null ||
|
||||
apiKeyError !== null ||
|
||||
validateMutation.isPending
|
||||
: !workspace.trim() ||
|
||||
workspaceError !== null ||
|
||||
validateMutation.isPending;
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClose={handleClose}>
|
||||
<ModalBody className="items-start border border-tertiary w-96">
|
||||
<BaseModalTitle
|
||||
title={
|
||||
showConfigurationFields
|
||||
? t(I18nKey.PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE, {
|
||||
platform: platformName,
|
||||
})
|
||||
: t(I18nKey.PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE)
|
||||
}
|
||||
/>
|
||||
<BaseModalDescription>
|
||||
{showConfigurationFields ? (
|
||||
<Trans
|
||||
i18nKey={I18nKey.PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION}
|
||||
components={{
|
||||
a: (
|
||||
<a
|
||||
href="https://docs.all-hands.dev/usage/cloud/openhands-cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
Check the document for more information
|
||||
</a>
|
||||
),
|
||||
b: <b />,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p className="mt-4">
|
||||
<Trans
|
||||
i18nKey={
|
||||
I18nKey.PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION
|
||||
}
|
||||
components={{
|
||||
b: <b />,
|
||||
a: (
|
||||
<a
|
||||
href="https://docs.all-hands.dev/usage/cloud/openhands-cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
Check the document for more information
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</BaseModalDescription>
|
||||
<div className="w-full flex flex-col gap-4 mt-4">
|
||||
<div>
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<SettingsInput
|
||||
label={t(I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL)}
|
||||
placeholder={t(
|
||||
I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER,
|
||||
)}
|
||||
value={workspace}
|
||||
onChange={handleWorkspaceChange}
|
||||
className="w-full"
|
||||
type="text"
|
||||
pattern="^[a-zA-Z0-9\-_.]*$"
|
||||
isDisabled={!!existingWorkspace}
|
||||
/>
|
||||
</div>
|
||||
{existingWorkspace && onUnlink && (
|
||||
<BrandButton
|
||||
variant="secondary"
|
||||
onClick={onUnlink}
|
||||
data-testid="unlink-button"
|
||||
type="button"
|
||||
className="mb-0"
|
||||
>
|
||||
{t(I18nKey.PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL)}
|
||||
</BrandButton>
|
||||
)}
|
||||
</div>
|
||||
{workspaceError && (
|
||||
<p className="text-red-500 text-sm mt-2">{workspaceError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showConfigurationFields && (
|
||||
<>
|
||||
<div>
|
||||
<SettingsInput
|
||||
label={t(I18nKey.PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL)}
|
||||
placeholder={t(
|
||||
I18nKey.PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER,
|
||||
)}
|
||||
value={webhookSecret}
|
||||
onChange={handleWebhookSecretChange}
|
||||
className="w-full"
|
||||
type="password"
|
||||
/>
|
||||
{webhookSecretError && (
|
||||
<p className="text-red-500 text-sm mt-2">
|
||||
{webhookSecretError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<SettingsInput
|
||||
label={t(
|
||||
I18nKey.PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL,
|
||||
)}
|
||||
placeholder={t(
|
||||
I18nKey.PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_PLACEHOLDER,
|
||||
)}
|
||||
value={serviceAccountEmail}
|
||||
onChange={handleEmailChange}
|
||||
className="w-full"
|
||||
type="email"
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-red-500 text-sm mt-2">{emailError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<SettingsInput
|
||||
label={t(
|
||||
I18nKey.PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_LABEL,
|
||||
)}
|
||||
placeholder={t(
|
||||
I18nKey.PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER,
|
||||
)}
|
||||
value={serviceAccountApiKey}
|
||||
onChange={handleApiKeyChange}
|
||||
className="w-full"
|
||||
type="password"
|
||||
/>
|
||||
{apiKeyError && (
|
||||
<p className="text-red-500 text-sm mt-2">{apiKeyError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<SettingsSwitch
|
||||
testId="active-toggle"
|
||||
onToggle={setIsActive}
|
||||
isToggled={isActive}
|
||||
>
|
||||
{t(I18nKey.PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL)}
|
||||
</SettingsSwitch>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full mt-4">
|
||||
{/* Hide the connect/edit button if workspace exists but is not editable */}
|
||||
{(!existingWorkspace || isWorkspaceEditable) && (
|
||||
<BrandButton
|
||||
variant="primary"
|
||||
onClick={handleConnect}
|
||||
data-testid="connect-button"
|
||||
type="button"
|
||||
className="w-full"
|
||||
isDisabled={isConnectDisabled}
|
||||
>
|
||||
{(() => {
|
||||
if (existingWorkspace && showConfigurationFields) {
|
||||
return t(I18nKey.PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL);
|
||||
}
|
||||
return t(I18nKey.PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL);
|
||||
})()}
|
||||
</BrandButton>
|
||||
)}
|
||||
<BrandButton
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
data-testid="cancel-button"
|
||||
type="button"
|
||||
className="w-full"
|
||||
>
|
||||
{t(I18nKey.FEEDBACK$CANCEL_LABEL)}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface IntegrationButtonProps {
|
||||
isLoading: boolean;
|
||||
isLinked: boolean;
|
||||
onClick: () => void;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
export function IntegrationButton({
|
||||
isLoading,
|
||||
isLinked,
|
||||
onClick,
|
||||
"data-testid": dataTestId,
|
||||
}: IntegrationButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<BrandButton
|
||||
data-testid={dataTestId}
|
||||
variant={isLinked ? "secondary" : "primary"}
|
||||
onClick={onClick}
|
||||
isDisabled={isLoading}
|
||||
type="button"
|
||||
className="w-30 min-w-20"
|
||||
>
|
||||
{isLoading && t(I18nKey.SETTINGS$SAVING)}
|
||||
{!isLoading &&
|
||||
(isLinked
|
||||
? t(I18nKey.PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL)
|
||||
: t(I18nKey.PROJECT_MANAGEMENT$LINK_BUTTON_LABEL))}
|
||||
</BrandButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useIntegrationStatus } from "#/hooks/query/use-integration-status";
|
||||
import { useLinkIntegration } from "#/hooks/mutation/use-link-integration";
|
||||
import { useUnlinkIntegration } from "#/hooks/mutation/use-unlink-integration";
|
||||
import { useConfigureIntegration } from "#/hooks/mutation/use-configure-integration";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import {
|
||||
ConfigureButton,
|
||||
ConfigureModal,
|
||||
} from "#/components/features/settings/project-management/configure-modal";
|
||||
|
||||
interface IntegrationRowProps {
|
||||
platform: "jira" | "jira-dc" | "linear";
|
||||
platformName: string;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
export function IntegrationRow({
|
||||
platform,
|
||||
platformName,
|
||||
"data-testid": dataTestId,
|
||||
}: IntegrationRowProps) {
|
||||
const [isConfigureModalOpen, setConfigureModalOpen] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: integrationData, isLoading: isStatusLoading } =
|
||||
useIntegrationStatus(platform);
|
||||
|
||||
const linkMutation = useLinkIntegration(platform, {
|
||||
onSettled: () => {
|
||||
setConfigureModalOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const unlinkMutation = useUnlinkIntegration(platform, {
|
||||
onSettled: () => {
|
||||
setConfigureModalOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const configureMutation = useConfigureIntegration(platform, {
|
||||
onSettled: () => {
|
||||
setConfigureModalOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfigure = () => {
|
||||
setConfigureModalOpen(true);
|
||||
};
|
||||
|
||||
const handleLink = (workspace: string) => {
|
||||
linkMutation.mutate(workspace);
|
||||
};
|
||||
|
||||
const handleUnlink = () => {
|
||||
unlinkMutation.mutate();
|
||||
};
|
||||
|
||||
const handleConfigureConfirm = (data: {
|
||||
workspace: string;
|
||||
webhookSecret: string;
|
||||
serviceAccountEmail: string;
|
||||
serviceAccountApiKey: string;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
configureMutation.mutate(data);
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
isStatusLoading ||
|
||||
linkMutation.isPending ||
|
||||
unlinkMutation.isPending ||
|
||||
configureMutation.isPending;
|
||||
|
||||
// Determine if integration is active and workspace exists
|
||||
const isIntegrationActive = integrationData?.status === "active";
|
||||
const hasWorkspace = integrationData?.workspace;
|
||||
|
||||
// Determine button text based on integration state
|
||||
const buttonText =
|
||||
isIntegrationActive && hasWorkspace
|
||||
? t(I18nKey.PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL)
|
||||
: t(I18nKey.PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between" data-testid={dataTestId}>
|
||||
<span className="font-medium">{platformName}</span>
|
||||
<div className="flex items-center gap-6">
|
||||
<ConfigureButton
|
||||
onClick={handleConfigure}
|
||||
isDisabled={isLoading}
|
||||
text={buttonText}
|
||||
data-testid={`${platform}-configure-button`}
|
||||
/>
|
||||
</div>
|
||||
<ConfigureModal
|
||||
isOpen={isConfigureModalOpen}
|
||||
onClose={() => setConfigureModalOpen(false)}
|
||||
onConfirm={handleConfigureConfirm}
|
||||
onLink={handleLink}
|
||||
onUnlink={handleUnlink}
|
||||
platformName={platformName}
|
||||
platform={platform}
|
||||
integrationData={integrationData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { IntegrationRow } from "./integration-row";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
export function ProjectManagementIntegration() {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-1/4">
|
||||
<h3 className="text-xl font-medium text-white">
|
||||
{t(I18nKey.PROJECT_MANAGEMENT$TITLE)}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
{config?.FEATURE_FLAGS?.ENABLE_JIRA && (
|
||||
<IntegrationRow
|
||||
platform="jira"
|
||||
platformName="Jira Cloud"
|
||||
data-testid="jira-integration-row"
|
||||
/>
|
||||
)}
|
||||
{config?.FEATURE_FLAGS?.ENABLE_JIRA_DC && (
|
||||
<IntegrationRow
|
||||
platform="jira-dc"
|
||||
platformName="Jira Data Center"
|
||||
data-testid="jira-dc-integration-row"
|
||||
/>
|
||||
)}
|
||||
{config?.FEATURE_FLAGS?.ENABLE_LINEAR && (
|
||||
<IntegrationRow
|
||||
platform="linear"
|
||||
platformName="Linear"
|
||||
data-testid="linear-integration-row"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
import { AccountSettingsContextMenu } from "../context-menu/account-settings-context-menu";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
|
||||
interface UserActionsProps {
|
||||
onLogout: () => void;
|
||||
@@ -9,10 +10,12 @@ interface UserActionsProps {
|
||||
}
|
||||
|
||||
export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const [accountContextMenuIsVisible, setAccountContextMenuIsVisible] =
|
||||
React.useState(false);
|
||||
|
||||
const toggleAccountMenu = () => {
|
||||
// Always toggle the menu, even if user is undefined
|
||||
setAccountContextMenuIsVisible((prev) => !prev);
|
||||
};
|
||||
|
||||
@@ -25,6 +28,9 @@ export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
closeAccountMenu();
|
||||
};
|
||||
|
||||
// Always show the menu for authenticated users, even without user data
|
||||
const showMenu = accountContextMenuIsVisible && isAuthed;
|
||||
|
||||
return (
|
||||
<div data-testid="user-actions" className="w-8 h-8 relative cursor-pointer">
|
||||
<UserAvatar
|
||||
@@ -33,7 +39,7 @@ export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{accountContextMenuIsVisible && !!user && (
|
||||
{showMenu && (
|
||||
<AccountSettingsContextMenu
|
||||
onLogout={handleLogout}
|
||||
onClose={closeAccountMenu}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface UserAvatarProps {
|
||||
|
||||
export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipButton
|
||||
testId="user-avatar"
|
||||
|
||||
@@ -35,6 +35,11 @@ export function AuthModal({
|
||||
identityProvider: "bitbucket",
|
||||
});
|
||||
|
||||
const enterpriseSsoUrl = useAuthUrl({
|
||||
appMode: appMode || null,
|
||||
identityProvider: "enterprise_sso",
|
||||
});
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
@@ -56,6 +61,13 @@ export function AuthModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnterpriseSsoAuth = () => {
|
||||
if (enterpriseSsoUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = enterpriseSsoUrl;
|
||||
}
|
||||
};
|
||||
|
||||
// Only show buttons if providers are configured and include the specific provider
|
||||
const showGithub =
|
||||
providersConfigured &&
|
||||
@@ -69,6 +81,10 @@ export function AuthModal({
|
||||
providersConfigured &&
|
||||
providersConfigured.length > 0 &&
|
||||
providersConfigured.includes("bitbucket");
|
||||
const showEnterpriseSso =
|
||||
providersConfigured &&
|
||||
providersConfigured.length > 0 &&
|
||||
providersConfigured.includes("enterprise_sso");
|
||||
|
||||
// Check if no providers are configured
|
||||
const noProvidersConfigured =
|
||||
@@ -126,6 +142,17 @@ export function AuthModal({
|
||||
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
|
||||
</BrandButton>
|
||||
)}
|
||||
|
||||
{showEnterpriseSso && (
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleEnterpriseSsoAuth}
|
||||
className="w-full"
|
||||
>
|
||||
{t(I18nKey.ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO)}
|
||||
</BrandButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ interface ConversationSubscriptionsContextType {
|
||||
subscribeToConversation: (options: {
|
||||
conversationId: string;
|
||||
sessionApiKey: string | null;
|
||||
providersSet: ("github" | "gitlab" | "bitbucket")[];
|
||||
providersSet: ("github" | "gitlab" | "bitbucket" | "enterprise_sso")[];
|
||||
baseUrl: string;
|
||||
onEvent?: (event: unknown, conversationId: string) => void;
|
||||
}) => void;
|
||||
@@ -135,7 +135,7 @@ export function ConversationSubscriptionsProvider({
|
||||
(options: {
|
||||
conversationId: string;
|
||||
sessionApiKey: string | null;
|
||||
providersSet: ("github" | "gitlab" | "bitbucket")[];
|
||||
providersSet: ("github" | "gitlab" | "bitbucket" | "enterprise_sso")[];
|
||||
baseUrl: string;
|
||||
onEvent?: (event: unknown, conversationId: string) => void;
|
||||
}) => {
|
||||
@@ -226,6 +226,7 @@ export function ConversationSubscriptionsProvider({
|
||||
});
|
||||
|
||||
socket.on("connect_error", (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Socket for conversation ${conversationId} CONNECTION ERROR:`,
|
||||
error,
|
||||
@@ -233,6 +234,7 @@ export function ConversationSubscriptionsProvider({
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Socket for conversation ${conversationId} DISCONNECTED! Reason:`,
|
||||
reason,
|
||||
|
||||
72
frontend/src/hooks/mutation/use-configure-integration.ts
Normal file
72
frontend/src/hooks/mutation/use-configure-integration.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
|
||||
interface ConfigureIntegrationData {
|
||||
workspace: string;
|
||||
webhookSecret: string;
|
||||
serviceAccountEmail: string;
|
||||
serviceAccountApiKey: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export function useConfigureIntegration(
|
||||
platform: "jira" | "jira-dc" | "linear",
|
||||
{
|
||||
onSettled,
|
||||
}: {
|
||||
onSettled: () => void;
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: ConfigureIntegrationData) => {
|
||||
const input = {
|
||||
workspace_name: data.workspace,
|
||||
webhook_secret: data.webhookSecret,
|
||||
svc_acc_email: data.serviceAccountEmail,
|
||||
svc_acc_api_key: data.serviceAccountApiKey,
|
||||
is_active: data.isActive,
|
||||
};
|
||||
|
||||
const response = await openHands.post(
|
||||
`/integration/${platform}/workspaces`,
|
||||
input,
|
||||
);
|
||||
|
||||
const { success, redirect, authorizationUrl } = response.data;
|
||||
|
||||
if (success) {
|
||||
if (redirect) {
|
||||
if (authorizationUrl) {
|
||||
window.location.href = authorizationUrl;
|
||||
} else {
|
||||
throw new Error("Could not get authorization URL from the server.");
|
||||
}
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
throw new Error("Configuration failed");
|
||||
}
|
||||
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["integration-status", platform],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
|
||||
},
|
||||
onSettled,
|
||||
});
|
||||
}
|
||||
60
frontend/src/hooks/mutation/use-link-integration.ts
Normal file
60
frontend/src/hooks/mutation/use-link-integration.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
|
||||
export function useLinkIntegration(
|
||||
platform: "jira" | "jira-dc" | "linear",
|
||||
{
|
||||
onSettled,
|
||||
}: {
|
||||
onSettled: () => void;
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (workspace: string) => {
|
||||
const input = {
|
||||
workspace_name: workspace,
|
||||
};
|
||||
|
||||
const response = await openHands.post(
|
||||
`/integration/${platform}/workspaces/link`,
|
||||
input,
|
||||
);
|
||||
|
||||
const { success, redirect, authorizationUrl } = response.data;
|
||||
|
||||
if (success) {
|
||||
if (redirect) {
|
||||
if (authorizationUrl) {
|
||||
window.location.href = authorizationUrl;
|
||||
} else {
|
||||
throw new Error("Could not get authorization URL from the server.");
|
||||
}
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
throw new Error("Link integration failed");
|
||||
}
|
||||
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["integration-status", platform],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
|
||||
},
|
||||
onSettled,
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import {
|
||||
BatchFeedbackData,
|
||||
getFeedbackQueryKey,
|
||||
} from "../query/use-batch-feedback";
|
||||
|
||||
type SubmitConversationFeedbackArgs = {
|
||||
rating: number;
|
||||
@@ -12,7 +15,6 @@ type SubmitConversationFeedbackArgs = {
|
||||
export const useSubmitConversationFeedback = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ rating, eventId, reason }: SubmitConversationFeedbackArgs) =>
|
||||
@@ -22,18 +24,56 @@ export const useSubmitConversationFeedback = () => {
|
||||
eventId,
|
||||
reason,
|
||||
),
|
||||
onSuccess: (_, { eventId }) => {
|
||||
// Invalidate the feedback existence query to trigger a refetch
|
||||
onMutate: async ({ rating, eventId, reason }) => {
|
||||
if (!eventId) return { previousFeedback: null };
|
||||
|
||||
// Get the query key for the feedback data
|
||||
const queryKey = getFeedbackQueryKey(conversationId);
|
||||
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousFeedback =
|
||||
queryClient.getQueryData<Record<string, BatchFeedbackData>>(queryKey);
|
||||
|
||||
// Optimistically update the cache
|
||||
queryClient.setQueryData<Record<string, BatchFeedbackData>>(
|
||||
queryKey,
|
||||
(old = {}) => {
|
||||
const newData = { ...old };
|
||||
newData[eventId.toString()] = {
|
||||
exists: true,
|
||||
rating,
|
||||
reason,
|
||||
metadata: { source: "likert-scale" },
|
||||
};
|
||||
return newData;
|
||||
},
|
||||
);
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { previousFeedback };
|
||||
},
|
||||
onError: (error, { eventId }, context) => {
|
||||
// Roll back to the previous value on error
|
||||
if (context?.previousFeedback && eventId) {
|
||||
queryClient.setQueryData(
|
||||
getFeedbackQueryKey(conversationId),
|
||||
context.previousFeedback,
|
||||
);
|
||||
}
|
||||
// Log error but don't show toast - user will just see the UI stay in unsubmitted state
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
},
|
||||
onSettled: (_, __, { eventId }) => {
|
||||
if (eventId) {
|
||||
// Invalidate both the old and new query keys to ensure consistency
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["feedback", "exists", conversationId, eventId],
|
||||
queryKey: getFeedbackQueryKey(conversationId),
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
// Log error but don't show toast - user will just see the UI stay in unsubmitted state
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(t("FEEDBACK$FAILED_TO_SUBMIT"), error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
38
frontend/src/hooks/mutation/use-unlink-integration.ts
Normal file
38
frontend/src/hooks/mutation/use-unlink-integration.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
|
||||
export function useUnlinkIntegration(
|
||||
platform: "jira" | "jira-dc" | "linear",
|
||||
{
|
||||
onSettled,
|
||||
}: {
|
||||
onSettled: () => void;
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () =>
|
||||
openHands.post(`/integration/${platform}/workspaces/unlink`),
|
||||
onSuccess: () => {
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["integration-status", platform],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
|
||||
},
|
||||
onSettled,
|
||||
});
|
||||
}
|
||||
43
frontend/src/hooks/mutation/use-validate-integration.ts
Normal file
43
frontend/src/hooks/mutation/use-validate-integration.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
|
||||
export function useValidateIntegration(
|
||||
platform: "jira" | "jira-dc" | "linear",
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: (data: any) => void;
|
||||
onError: (error: any) => void;
|
||||
},
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (workspace?: string) => {
|
||||
const workspaceParam = workspace ? `/${workspace}` : "";
|
||||
return openHands.get(
|
||||
`/integration/${platform}/workspaces/validate${workspaceParam}`,
|
||||
);
|
||||
},
|
||||
onSuccess,
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
onError(error);
|
||||
} else {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(
|
||||
errorMessage ||
|
||||
t(I18nKey.PROJECT_MANAGEMENT$VALIDATE_INTEGRATION_ERROR),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
52
frontend/src/hooks/query/use-batch-feedback.ts
Normal file
52
frontend/src/hooks/query/use-batch-feedback.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
|
||||
|
||||
export interface BatchFeedbackData {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Query key factory to ensure consistency across hooks
|
||||
export const getFeedbackQueryKey = (conversationId?: string) =>
|
||||
["feedback", "data", conversationId] as const;
|
||||
|
||||
// Query key factory for individual feedback existence
|
||||
export const getFeedbackExistsQueryKey = (
|
||||
conversationId: string,
|
||||
eventId: number,
|
||||
) => ["feedback", "exists", conversationId, eventId] as const;
|
||||
|
||||
export const useBatchFeedback = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: config } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: getFeedbackQueryKey(conversationId),
|
||||
queryFn: () => OpenHands.getBatchFeedback(conversationId!),
|
||||
enabled: runtimeIsReady && !!conversationId && config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
// Update individual feedback cache entries when batch data changes
|
||||
React.useEffect(() => {
|
||||
if (query.data && conversationId) {
|
||||
Object.entries(query.data).forEach(([eventId, feedback]) => {
|
||||
queryClient.setQueryData(
|
||||
getFeedbackExistsQueryKey(conversationId, parseInt(eventId, 10)),
|
||||
feedback,
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [query.data, conversationId, queryClient]);
|
||||
|
||||
return query;
|
||||
};
|
||||
@@ -1,25 +1,28 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { BatchFeedbackData, getFeedbackQueryKey } from "./use-batch-feedback";
|
||||
|
||||
export interface FeedbackData {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
}
|
||||
export type FeedbackData = BatchFeedbackData;
|
||||
|
||||
export const useFeedbackExists = (eventId?: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
return useQuery<FeedbackData>({
|
||||
queryKey: ["feedback", "exists", conversationId, eventId],
|
||||
queryKey: [...getFeedbackQueryKey(conversationId), eventId],
|
||||
queryFn: () => {
|
||||
if (!eventId) return { exists: false };
|
||||
return OpenHands.checkFeedbackExists(conversationId, eventId);
|
||||
|
||||
// Try to get the data from the batch cache
|
||||
const batchData = queryClient.getQueryData<
|
||||
Record<string, BatchFeedbackData>
|
||||
>(getFeedbackQueryKey(conversationId));
|
||||
|
||||
return batchData?.[eventId.toString()] ?? { exists: false };
|
||||
},
|
||||
enabled: !!eventId && config?.APP_MODE === "saas",
|
||||
enabled: !!eventId && !!conversationId && config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
|
||||
export const useGetSecrets = () => {
|
||||
const { data: config } = useConfig();
|
||||
const { providers } = useUserProviders();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
|
||||
const isOss = config?.APP_MODE === "oss";
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["secrets"],
|
||||
queryFn: SecretsService.getSecrets,
|
||||
enabled: isOss || providers.length > 0,
|
||||
enabled: isOss || isAuthed, // Enable regardless of providers
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,16 +3,16 @@ import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
|
||||
export const useGitUser = () => {
|
||||
const { providers } = useUserProviders();
|
||||
const { data: config } = useConfig();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
|
||||
const user = useQuery({
|
||||
queryKey: ["user"],
|
||||
queryFn: OpenHands.getGitUser,
|
||||
enabled: !!config?.APP_MODE && providers.length > 0,
|
||||
enabled: !!config?.APP_MODE && isAuthed, // Enable regardless of providers
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
|
||||
22
frontend/src/hooks/query/use-integration-status.ts
Normal file
22
frontend/src/hooks/query/use-integration-status.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
|
||||
export function useIntegrationStatus(platform: "jira" | "jira-dc" | "linear") {
|
||||
return useQuery({
|
||||
queryKey: ["integration-status", platform],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await openHands.get(
|
||||
`/integration/${platform}/workspaces/link`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useRepositoryMicroagentContent = (
|
||||
owner: string,
|
||||
repo: string,
|
||||
filePath: string,
|
||||
cacheDisabled: boolean = false,
|
||||
) =>
|
||||
useQuery({
|
||||
queryKey: ["repository", "microagent", "content", owner, repo, filePath],
|
||||
queryFn: () =>
|
||||
OpenHands.getRepositoryMicroagentContent(owner, repo, filePath),
|
||||
enabled: !!owner && !!repo && !!filePath,
|
||||
staleTime: cacheDisabled ? 0 : 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: cacheDisabled ? 0 : 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
@@ -31,6 +31,11 @@ export const useAutoLogin = () => {
|
||||
identityProvider: "bitbucket",
|
||||
});
|
||||
|
||||
const enterpriseSsoUrl = useAuthUrl({
|
||||
appMode: config?.APP_MODE || null,
|
||||
identityProvider: "enterprise_sso",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Only auto-login in SAAS mode
|
||||
if (config?.APP_MODE !== "saas") {
|
||||
@@ -60,6 +65,8 @@ export const useAutoLogin = () => {
|
||||
authUrl = gitlabAuthUrl;
|
||||
} else if (loginMethod === LoginMethod.BITBUCKET) {
|
||||
authUrl = bitbucketAuthUrl;
|
||||
} else if (loginMethod === LoginMethod.ENTERPRISE_SSO) {
|
||||
authUrl = enterpriseSsoUrl;
|
||||
}
|
||||
|
||||
// If we have an auth URL, redirect to it
|
||||
@@ -80,5 +87,6 @@ export const useAutoLogin = () => {
|
||||
githubAuthUrl,
|
||||
gitlabAuthUrl,
|
||||
bitbucketAuthUrl,
|
||||
enterpriseSsoUrl,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -22,8 +22,18 @@ const DEFAULT_TERMINAL_CONFIG: UseTerminalConfig = {
|
||||
commands: [],
|
||||
};
|
||||
|
||||
const renderCommand = (command: Command, terminal: Terminal) => {
|
||||
const { content } = command;
|
||||
const renderCommand = (
|
||||
command: Command,
|
||||
terminal: Terminal,
|
||||
isUserInput: boolean = false,
|
||||
) => {
|
||||
const { content, type } = command;
|
||||
|
||||
// Skip rendering user input commands that come from the event stream
|
||||
// as they've already been displayed in the terminal as the user typed
|
||||
if (type === "input" && isUserInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.writeln(
|
||||
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
|
||||
@@ -123,7 +133,9 @@ export const useTerminal = ({
|
||||
if (commands[i].type === "input") {
|
||||
terminal.current.write("$ ");
|
||||
}
|
||||
renderCommand(commands[i], terminal.current);
|
||||
// Don't pass isUserInput=true here because we're initializing the terminal
|
||||
// and need to show all previous commands
|
||||
renderCommand(commands[i], terminal.current, false);
|
||||
}
|
||||
lastCommandIndex.current = commands.length;
|
||||
}
|
||||
@@ -144,7 +156,9 @@ export const useTerminal = ({
|
||||
let lastCommandType = "";
|
||||
for (let i = lastCommandIndex.current; i < commands.length; i += 1) {
|
||||
lastCommandType = commands[i].type;
|
||||
renderCommand(commands[i], terminal.current);
|
||||
// Pass true for isUserInput to skip rendering user input commands
|
||||
// that have already been displayed as the user typed
|
||||
renderCommand(commands[i], terminal.current, true);
|
||||
}
|
||||
lastCommandIndex.current = commands.length;
|
||||
if (lastCommandType === "output") {
|
||||
|
||||
@@ -107,6 +107,8 @@ export enum I18nKey {
|
||||
SETTINGS$NAV_CREDITS = "SETTINGS$NAV_CREDITS",
|
||||
SETTINGS$NAV_SECRETS = "SETTINGS$NAV_SECRETS",
|
||||
SETTINGS$NAV_API_KEYS = "SETTINGS$NAV_API_KEYS",
|
||||
SETTINGS$GITHUB = "SETTINGS$GITHUB",
|
||||
SETTINGS$SLACK = "SETTINGS$SLACK",
|
||||
SETTINGS$NAV_LLM = "SETTINGS$NAV_LLM",
|
||||
GIT$MERGE_REQUEST = "GIT$MERGE_REQUEST",
|
||||
GIT$GITLAB_API = "GIT$GITLAB_API",
|
||||
@@ -557,6 +559,7 @@ export enum I18nKey {
|
||||
GITHUB$CONNECT_TO_GITHUB = "GITHUB$CONNECT_TO_GITHUB",
|
||||
GITLAB$CONNECT_TO_GITLAB = "GITLAB$CONNECT_TO_GITLAB",
|
||||
BITBUCKET$CONNECT_TO_BITBUCKET = "BITBUCKET$CONNECT_TO_BITBUCKET",
|
||||
ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO = "ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO",
|
||||
AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER = "AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER",
|
||||
WAITLIST$JOIN_WAITLIST = "WAITLIST$JOIN_WAITLIST",
|
||||
ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS = "ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS",
|
||||
@@ -734,4 +737,29 @@ export enum I18nKey {
|
||||
MICROAGENT_MANAGEMENT$WHAT_YOU_WOULD_LIKE_TO_KNOW_ABOUT_THIS_REPO = "MICROAGENT_MANAGEMENT$WHAT_YOU_WOULD_LIKE_TO_KNOW_ABOUT_THIS_REPO",
|
||||
MICROAGENT_MANAGEMENT$DESCRIBE_WHAT_TO_KNOW_ABOUT_THIS_REPO = "MICROAGENT_MANAGEMENT$DESCRIBE_WHAT_TO_KNOW_ABOUT_THIS_REPO",
|
||||
MICROAGENT_MANAGEMENT$UPDATE_MICROAGENT_MODAL_DESCRIPTION = "MICROAGENT_MANAGEMENT$UPDATE_MICROAGENT_MODAL_DESCRIPTION",
|
||||
PROJECT_MANAGEMENT$TITLE = "PROJECT_MANAGEMENT$TITLE",
|
||||
PROJECT_MANAGEMENT$VALIDATE_INTEGRATION_ERROR = "PROJECT_MANAGEMENT$VALIDATE_INTEGRATION_ERROR",
|
||||
PROJECT_MANAGEMENT$LINK_BUTTON_LABEL = "PROJECT_MANAGEMENT$LINK_BUTTON_LABEL",
|
||||
PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL = "PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL",
|
||||
PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE = "PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE",
|
||||
PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL = "PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL",
|
||||
PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL = "PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL",
|
||||
PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL = "PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL",
|
||||
PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER = "PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER",
|
||||
PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL = "PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL",
|
||||
PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER = "PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER",
|
||||
PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL = "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL",
|
||||
PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_PLACEHOLDER = "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_PLACEHOLDER",
|
||||
PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_LABEL = "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_LABEL",
|
||||
PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER = "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER",
|
||||
PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL = "PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL",
|
||||
PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL = "PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL",
|
||||
PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION",
|
||||
PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE",
|
||||
PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION = "PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION",
|
||||
PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR = "PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR",
|
||||
PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR = "PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR",
|
||||
PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR",
|
||||
PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR",
|
||||
MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT = "MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT",
|
||||
}
|
||||
|
||||
@@ -1711,6 +1711,38 @@
|
||||
"de": "API-Schlüssel",
|
||||
"uk": "API ключі"
|
||||
},
|
||||
"SETTINGS$GITHUB": {
|
||||
"en": "GitHub",
|
||||
"ja": "GitHub",
|
||||
"zh-CN": "GitHub",
|
||||
"zh-TW": "GitHub",
|
||||
"ko-KR": "GitHub",
|
||||
"no": "GitHub",
|
||||
"it": "GitHub",
|
||||
"pt": "GitHub",
|
||||
"es": "GitHub",
|
||||
"ar": "GitHub",
|
||||
"fr": "GitHub",
|
||||
"tr": "GitHub",
|
||||
"de": "GitHub",
|
||||
"uk": "GitHub"
|
||||
},
|
||||
"SETTINGS$SLACK": {
|
||||
"en": "Slack",
|
||||
"ja": "Slack",
|
||||
"zh-CN": "Slack",
|
||||
"zh-TW": "Slack",
|
||||
"ko-KR": "Slack",
|
||||
"no": "Slack",
|
||||
"it": "Slack",
|
||||
"pt": "Slack",
|
||||
"es": "Slack",
|
||||
"ar": "Slack",
|
||||
"fr": "Slack",
|
||||
"tr": "Slack",
|
||||
"de": "Slack",
|
||||
"uk": "Slack"
|
||||
},
|
||||
"SETTINGS$NAV_LLM": {
|
||||
"en": "LLM",
|
||||
"ja": "LLM",
|
||||
@@ -4432,20 +4464,20 @@
|
||||
"uk": "Від'єднано"
|
||||
},
|
||||
"CHAT_INTERFACE$CONNECTING": {
|
||||
"en": "Connecting",
|
||||
"ja": "接続中",
|
||||
"zh-CN": "正在连接",
|
||||
"zh-TW": "正在連接",
|
||||
"ko-KR": "연결 중",
|
||||
"no": "Kobler til",
|
||||
"it": "Connessione in corso",
|
||||
"pt": "Conectando",
|
||||
"es": "Conectando",
|
||||
"ar": "جاري الاتصال",
|
||||
"fr": "Connexion en cours",
|
||||
"tr": "Bağlanıyor",
|
||||
"de": "Verbindung wird hergestellt",
|
||||
"uk": "З'єднання"
|
||||
"en": "Connecting... (this may take 1-2 minutes)",
|
||||
"ja": "接続中...(1-2分かかる場合があります)",
|
||||
"zh-CN": "正在连接...(这可能需要1-2分钟)",
|
||||
"zh-TW": "正在連接...(這可能需要1-2分鐘)",
|
||||
"ko-KR": "연결 중... (1-2분 정도 걸릴 수 있습니다)",
|
||||
"no": "Kobler til... (dette kan ta 1-2 minutter)",
|
||||
"it": "Connessione in corso... (potrebbe richiedere 1-2 minuti)",
|
||||
"pt": "Conectando... (isso pode levar 1-2 minutos)",
|
||||
"es": "Conectando... (esto puede tomar 1-2 minutos)",
|
||||
"ar": "جاري الاتصال... (قد يستغرق هذا 1-2 دقيقة)",
|
||||
"fr": "Connexion en cours... (cela peut prendre 1-2 minutes)",
|
||||
"tr": "Bağlanıyor... (bu 1-2 dakika sürebilir)",
|
||||
"de": "Verbindung wird hergestellt... (dies kann 1-2 Minuten dauern)",
|
||||
"uk": "З'єднання... (це може зайняти 1-2 хвилини)"
|
||||
},
|
||||
"CHAT_INTERFACE$STOPPED": {
|
||||
"en": "Stopped",
|
||||
@@ -6192,20 +6224,20 @@
|
||||
"uk": "Ви можете знайти свій ключ API OpenHands у"
|
||||
},
|
||||
"SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX": {
|
||||
"en": "tab of OpenHands Cloud.",
|
||||
"ja": "タブで確認できます。",
|
||||
"zh-CN": "标签页中找到您的OpenHands API密钥。",
|
||||
"zh-TW": "標籤頁中找到您的OpenHands API密鑰。",
|
||||
"ko-KR": "탭에서 찾을 수 있습니다.",
|
||||
"no": "-fanen i OpenHands Cloud.",
|
||||
"it": "scheda di OpenHands Cloud.",
|
||||
"pt": "guia do OpenHands Cloud.",
|
||||
"es": "pestaña de OpenHands Cloud.",
|
||||
"ar": "علامة التبويب في OpenHands Cloud.",
|
||||
"fr": "l'onglet d'OpenHands Cloud.",
|
||||
"tr": "OpenHands Cloud'un sekmesinde bulabilirsiniz.",
|
||||
"de": "Tab von OpenHands Cloud.",
|
||||
"uk": "вкладці OpenHands Cloud."
|
||||
"en": "tab of OpenHands Cloud. LLM usage is billed at the providers' rates with no markup. Details: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"ja": "タブで確認できます。LLMの使用料金は、プロバイダーの料金でマークアップなしで請求されます。詳細: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"zh-CN": "标签页中找到您的OpenHands API密钥。LLM使用费用按提供商费率计费,无加价。详情: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"zh-TW": "標籤頁中找到您的OpenHands API密鑰。LLM使用費用按提供商費率計費,無加價。詳情: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"ko-KR": "탭에서 찾을 수 있습니다. LLM 사용료는 제공업체 요금으로 마크업 없이 청구됩니다. 자세한 내용: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"no": "-fanen i OpenHands Cloud. LLM-bruk faktureres til leverandørenes priser uten påslag. Detaljer: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"it": "scheda di OpenHands Cloud. L'utilizzo di LLM viene fatturato alle tariffe dei fornitori senza ricarico. Dettagli: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"pt": "guia do OpenHands Cloud. O uso de LLM é cobrado nas tarifas dos provedores sem markup. Detalhes: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"es": "pestaña de OpenHands Cloud. El uso de LLM se factura a las tarifas de los proveedores sin recargo. Detalles: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"ar": "علامة التبويب في OpenHands Cloud. يتم فوترة استخدام LLM بأسعار المزودين بدون زيادة. التفاصيل: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"fr": "l'onglet d'OpenHands Cloud. L'utilisation de LLM est facturée aux tarifs des fournisseurs sans majoration. Détails : https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"tr": "OpenHands Cloud'un sekmesinde bulabilirsiniz. LLM kullanımı, sağlayıcıların oranlarında ek ücret olmadan faturalandırılır. Ayrıntılar: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"de": "Tab von OpenHands Cloud. LLM-Nutzung wird zu Anbieterpreisen ohne Aufschlag abgerechnet. Details: https://docs.all-hands.dev/usage/llms/openhands-llms",
|
||||
"uk": "вкладці OpenHands Cloud. Використання LLM оплачується за тарифами провайдерів без надбавки. Деталі: https://docs.all-hands.dev/usage/llms/openhands-llms"
|
||||
},
|
||||
"SETTINGS$CREATE_API_KEY": {
|
||||
"en": "Create API Key",
|
||||
@@ -6640,20 +6672,20 @@
|
||||
"uk": "Підключення до середовища виконання..."
|
||||
},
|
||||
"STATUS$STARTING_RUNTIME": {
|
||||
"en": "Starting runtime...",
|
||||
"zh-CN": "启动运行时...",
|
||||
"zh-TW": "啟動執行時...",
|
||||
"de": "Laufzeitumgebung wird gestartet...",
|
||||
"ko-KR": "런타임 시작 중...",
|
||||
"no": "Starter kjøretidsmiljø...",
|
||||
"it": "Avvio dell'ambiente di esecuzione...",
|
||||
"pt": "Iniciando o ambiente de execução...",
|
||||
"es": "Iniciando el entorno de ejecución...",
|
||||
"ar": "جارٍ بدء بيئة التشغيل...",
|
||||
"fr": "Démarrage de l'environnement d'exécution...",
|
||||
"tr": "Çalışma zamanı başlatılıyor...",
|
||||
"ja": "ランタイムを開始中",
|
||||
"uk": "Запуск середовища виконання..."
|
||||
"en": "Starting runtime... (this may take 1-2 minutes)",
|
||||
"zh-CN": "启动运行时...(这可能需要1-2分钟)",
|
||||
"zh-TW": "啟動執行時...(這可能需要1-2分鐘)",
|
||||
"de": "Laufzeitumgebung wird gestartet... (dies kann 1-2 Minuten dauern)",
|
||||
"ko-KR": "런타임 시작 중... (1-2분 정도 걸릴 수 있습니다)",
|
||||
"no": "Starter kjøretidsmiljø... (dette kan ta 1-2 minutter)",
|
||||
"it": "Avvio dell'ambiente di esecuzione... (potrebbe richiedere 1-2 minuti)",
|
||||
"pt": "Iniciando o ambiente de execução... (isso pode levar 1-2 minutos)",
|
||||
"es": "Iniciando el entorno de ejecución... (esto puede tomar 1-2 minutos)",
|
||||
"ar": "جارٍ بدء بيئة التشغيل... (قد يستغرق هذا 1-2 دقيقة)",
|
||||
"fr": "Démarrage de l'environnement d'exécution... (cela peut prendre 1-2 minutes)",
|
||||
"tr": "Çalışma zamanı başlatılıyor... (bu 1-2 dakika sürebilir)",
|
||||
"ja": "ランタイムを開始中...(1-2分かかる場合があります)",
|
||||
"uk": "Запуск середовища виконання... (це може зайняти 1-2 хвилини)"
|
||||
},
|
||||
"STATUS$SETTING_UP_WORKSPACE": {
|
||||
"en": "Setting up workspace...",
|
||||
@@ -8911,6 +8943,22 @@
|
||||
"tr": "Bitbucket'a bağlan",
|
||||
"uk": "Увійти за допомогою Bitbucket"
|
||||
},
|
||||
"ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO": {
|
||||
"en": "Login with Enterprise SSO",
|
||||
"ja": "エンタープライズSSOでログイン",
|
||||
"zh-CN": "使用企业SSO登录",
|
||||
"zh-TW": "使用企業SSO登入",
|
||||
"ko-KR": "엔터프라이즈 SSO로 로그인",
|
||||
"de": "Mit Enterprise SSO anmelden",
|
||||
"no": "Logg inn med Enterprise SSO",
|
||||
"it": "Accedi con Enterprise SSO",
|
||||
"pt": "Entrar com Enterprise SSO",
|
||||
"es": "Iniciar sesión con Enterprise SSO",
|
||||
"ar": "تسجيل الدخول باستخدام Enterprise SSO",
|
||||
"fr": "Se connecter avec Enterprise SSO",
|
||||
"tr": "Enterprise SSO ile giriş yap",
|
||||
"uk": "Увійти за допомогою Enterprise SSO"
|
||||
},
|
||||
"AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER": {
|
||||
"en": "Log in to OpenHands",
|
||||
"ja": "IDプロバイダーでサインイン",
|
||||
@@ -10704,20 +10752,20 @@
|
||||
"uk": "Відключити токени"
|
||||
},
|
||||
"API$TAVILY_KEY_EXAMPLE": {
|
||||
"en": "sk-tavily-...",
|
||||
"ja": "sk-tavily-...",
|
||||
"zh-CN": "sk-tavily-...",
|
||||
"zh-TW": "sk-tavily-...",
|
||||
"ko-KR": "sk-tavily-...",
|
||||
"no": "sk-tavily-...",
|
||||
"it": "sk-tavily-...",
|
||||
"pt": "sk-tavily-...",
|
||||
"es": "sk-tavily-...",
|
||||
"ar": "sk-tavily-...",
|
||||
"fr": "sk-tavily-...",
|
||||
"tr": "sk-tavily-...",
|
||||
"de": "sk-tavily-...",
|
||||
"uk": "sk-tavily-..."
|
||||
"en": "tvly-dev-...",
|
||||
"ja": "tvly-dev-...",
|
||||
"zh-CN": "tvly-dev-...",
|
||||
"zh-TW": "tvly-dev-...",
|
||||
"ko-KR": "tvly-dev-...",
|
||||
"no": "tvly-dev-...",
|
||||
"it": "tvly-dev-...",
|
||||
"pt": "tvly-dev-...",
|
||||
"es": "tvly-dev-...",
|
||||
"ar": "tvly-dev-...",
|
||||
"fr": "tvly-dev-...",
|
||||
"tr": "tvly-dev-...",
|
||||
"de": "tvly-dev-...",
|
||||
"uk": "tvly-dev-..."
|
||||
},
|
||||
"API$TVLY_KEY_EXAMPLE": {
|
||||
"en": "tvly-...",
|
||||
@@ -11742,5 +11790,405 @@
|
||||
"tr": "OpenHands, talimatlarınıza göre mikro ajanı güncelleyecektir.",
|
||||
"de": "OpenHands aktualisiert den Microagenten basierend auf Ihren Anweisungen.",
|
||||
"uk": "OpenHands оновить мікроагента відповідно до ваших інструкцій."
|
||||
},
|
||||
"PROJECT_MANAGEMENT$TITLE": {
|
||||
"en": "Project Management",
|
||||
"ja": "プロジェクト管理",
|
||||
"zh-CN": "项目管理",
|
||||
"zh-TW": "專案管理",
|
||||
"ko-KR": "프로젝트 관리",
|
||||
"no": "Prosjektledelse",
|
||||
"it": "Gestione del progetto",
|
||||
"pt": "Gerenciamento de projetos",
|
||||
"es": "Gestión de proyectos",
|
||||
"ar": "إدارة المشاريع",
|
||||
"fr": "Gestion de projet",
|
||||
"tr": "Proje Yönetimi",
|
||||
"de": "Projektmanagement",
|
||||
"uk": "Управління проектами"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$VALIDATE_INTEGRATION_ERROR": {
|
||||
"en": "Failed to validate integration. Please try again later.",
|
||||
"ja": "統合の検証に失敗しました。しばらくしてからもう一度お試しください.",
|
||||
"zh-CN": "无法验证集成。请稍后重试.",
|
||||
"zh-TW": "無法驗證整合。請稍後重試.",
|
||||
"ko-KR": "통합을 검증하지 못했습니다. 나중에 다시 시도해 주세요.",
|
||||
"no": "Kunne ikke validere integrasjonen. Prøv på nytt senere.",
|
||||
"it": "Impossibile convalidare l'integrazione. Riprova più tardi.",
|
||||
"pt": "Falha ao validar a integração. Tente novamente mais tarde.",
|
||||
"es": "No se pudo validar la integración. Inténtelo de nuevo más tarde.",
|
||||
"ar": "فشل التحقق من التكامل. يُرجى المحاولة لاحقًا.",
|
||||
"fr": "Échec de la validation de l'intégration. Veuillez réessayer ultérieurement.",
|
||||
"tr": "Entegrasyon doğrulanamadı. Lütfen daha sonra tekrar deneyin.",
|
||||
"de": "Die Integration konnte nicht validiert werden. Bitte versuchen Sie es später noch einmal.",
|
||||
"uk": "Не вдалося перевірити інтеграцію. Спробуйте пізніше."
|
||||
},
|
||||
"PROJECT_MANAGEMENT$LINK_BUTTON_LABEL": {
|
||||
"en": "Link",
|
||||
"ja": "リンク",
|
||||
"zh-CN": "关联",
|
||||
"zh-TW": "關聯",
|
||||
"ko-KR": "링크",
|
||||
"no": "Link",
|
||||
"it": "Collegamento",
|
||||
"pt": "Link",
|
||||
"es": "Enlace",
|
||||
"ar": "وصلة",
|
||||
"fr": "Lien",
|
||||
"tr": "Bağlantı",
|
||||
"de": "Link",
|
||||
"uk": "Посилання"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL": {
|
||||
"en": "Unlink",
|
||||
"ja": "リンクを解除する",
|
||||
"zh-CN": "取消链接",
|
||||
"zh-TW": "取消連結",
|
||||
"ko-KR": "풀리다",
|
||||
"no": "Fjern tilknytningen",
|
||||
"it": "Scollega",
|
||||
"pt": "Desvincular",
|
||||
"es": "Desconectar",
|
||||
"ar": "إلغاء الارتباط",
|
||||
"fr": "Dissocier",
|
||||
"tr": "Bağlantıyı kaldır",
|
||||
"de": "Verknüpfung aufheben",
|
||||
"uk": "Від’єднати"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE": {
|
||||
"en": "Link Workspace",
|
||||
"ja": "ワークスペースをリンク",
|
||||
"zh-CN": "链接工作区",
|
||||
"zh-TW": "連結工作區",
|
||||
"ko-KR": "작업공간 링크",
|
||||
"no": "Link arbeidsområde",
|
||||
"it": "Collega area di lavoro",
|
||||
"pt": "Vincular espaço de trabalho",
|
||||
"es": "Vincular espacio de trabajo",
|
||||
"ar": "ربط مساحة العمل",
|
||||
"fr": "Lier l'espace de travail",
|
||||
"tr": "Çalışma Alanını Bağla",
|
||||
"de": "Arbeitsbereich verknüpfen",
|
||||
"uk": "Пов'язати робочу область"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL": {
|
||||
"en": "Configure",
|
||||
"ja": "設定する",
|
||||
"zh-CN": "配置",
|
||||
"zh-TW": "配置",
|
||||
"ko-KR": "구성",
|
||||
"no": "Konfigurer",
|
||||
"it": "Configura",
|
||||
"pt": "Configurar",
|
||||
"es": "Configurar",
|
||||
"ar": "تكوين",
|
||||
"fr": "Configurer",
|
||||
"tr": "Yapılandır",
|
||||
"de": "Konfigurieren",
|
||||
"uk": "Налаштувати"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL": {
|
||||
"en": "Edit",
|
||||
"ja": "編集",
|
||||
"zh-CN": "编辑",
|
||||
"zh-TW": "編輯",
|
||||
"ko-KR": "편집",
|
||||
"no": "Rediger",
|
||||
"it": "Modifica",
|
||||
"pt": "Editar",
|
||||
"es": "Editar",
|
||||
"ar": "تحرير",
|
||||
"fr": "Modifier",
|
||||
"tr": "Düzenle",
|
||||
"de": "Bearbeiten",
|
||||
"uk": "Редагувати"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL": {
|
||||
"en": "Workspace Name",
|
||||
"ja": "ワークスペース名",
|
||||
"zh-CN": "工作区名称",
|
||||
"zh-TW": "工作區名稱",
|
||||
"ko-KR": "작업공간 이름",
|
||||
"no": "Navn på arbeidsområde",
|
||||
"it": "Nome dell'area di lavoro",
|
||||
"pt": "Nome do espaço de trabalho",
|
||||
"es": "Nombre del espacio de trabajo",
|
||||
"ar": "اسم مساحة العمل",
|
||||
"fr": "Nom de l'espace de travail",
|
||||
"tr": "Çalışma Alanı Adı",
|
||||
"de": "Arbeitsbereichsname",
|
||||
"uk": "Назва робочої області"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER": {
|
||||
"en": "myworkspace",
|
||||
"ja": "私のワークスペース",
|
||||
"zh-CN": "我的工作空间",
|
||||
"zh-TW": "我的工作區",
|
||||
"ko-KR": "내워크스페이스",
|
||||
"no": "mittarbeidsområde",
|
||||
"it": "mioworkspace",
|
||||
"pt": "meuworkspace",
|
||||
"es": "miespaciodetrabajo",
|
||||
"ar": "مساحةعملي",
|
||||
"fr": "monworkspace",
|
||||
"tr": "benimworkspace",
|
||||
"de": "meinarbeitsbereich",
|
||||
"uk": "моя-робоча-область"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL": {
|
||||
"en": "Webhook Secret",
|
||||
"ja": "Webhook シークレット",
|
||||
"zh-CN": "Webhook 秘密",
|
||||
"zh-TW": "Webhook 秘密",
|
||||
"ko-KR": "웹훅 비밀",
|
||||
"no": "Webhook Secret",
|
||||
"it": "Segreto del webhook",
|
||||
"pt": "Segredo do webhook",
|
||||
"es": "Secreto del webhook",
|
||||
"ar": "سر الويب هوك",
|
||||
"fr": "Secret du webhook",
|
||||
"tr": "Web Kancası Sırrı",
|
||||
"de": "Webhook-Geheimnis",
|
||||
"uk": "Секрет вебхуку"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER": {
|
||||
"en": "whsec_abcd1234efgh5678ijkl",
|
||||
"ja": "whsec_abcd1234efgh5678ijkl",
|
||||
"zh-CN": "whsec_abcd1234efgh5678ijkl",
|
||||
"zh-TW": "whsec_abcd1234efgh5678ijkl",
|
||||
"ko-KR": "whsec_abcd1234efgh5678ijkl",
|
||||
"no": "whsec_abcd1234efgh5678ijkl",
|
||||
"it": "whsec_abcd1234efgh5678ijkl",
|
||||
"pt": "whsec_abcd1234efgh5678ijkl",
|
||||
"es": "whsec_abcd1234efgh5678ijkl",
|
||||
"ar": "whsec_abcd1234efgh5678ijkl",
|
||||
"fr": "whsec_abcd1234efgh5678ijkl",
|
||||
"tr": "whsec_abcd1234efgh5678ijkl",
|
||||
"de": "whsec_abcd1234efgh5678ijkl",
|
||||
"uk": "whsec_abcd1234efgh5678ijkl"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL": {
|
||||
"en": "Service Account Email",
|
||||
"ja": "サービスアカウントのメールアドレス",
|
||||
"zh-CN": "服务帐户电子邮件",
|
||||
"zh-TW": "服務帳號電子郵件",
|
||||
"ko-KR": "서비스 계정 이메일",
|
||||
"no": "Tjenestekonto e-post",
|
||||
"it": "E-mail dell'account di servizio",
|
||||
"pt": "E-mail da conta de serviço",
|
||||
"es": "Correo electrónico de cuenta de servicio",
|
||||
"ar": "البريد الإلكتروني لحساب الخدمة",
|
||||
"fr": "E-mail du compte de service",
|
||||
"tr": "Hizmet Hesabı E-postası",
|
||||
"de": "E-Mail-Adresse des Dienstkontos",
|
||||
"uk": "Електронна адреса облікового запису служби"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_PLACEHOLDER": {
|
||||
"en": "test@example.com",
|
||||
"ja": "test@example.com",
|
||||
"zh-CN": "test@example.com",
|
||||
"zh-TW": "test@example.com",
|
||||
"ko-KR": "test@example.com",
|
||||
"no": "test@example.com",
|
||||
"it": "test@example.com",
|
||||
"pt": "test@example.com",
|
||||
"es": "test@example.com",
|
||||
"ar": "test@example.com",
|
||||
"fr": "test@example.com",
|
||||
"tr": "test@example.com",
|
||||
"de": "test@example.com",
|
||||
"uk": "test@example.com"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_LABEL": {
|
||||
"en": "Service Account API Key",
|
||||
"ja": "サービスアカウントAPIキー",
|
||||
"zh-CN": "服务帐户 API 密钥",
|
||||
"zh-TW": "服務帳戶 API 金鑰",
|
||||
"ko-KR": "서비스 계정 API 키",
|
||||
"no": "Tjenestekonto API-nøkkel",
|
||||
"it": "Chiave API dell'account di servizio",
|
||||
"pt": "Chave de API da conta de serviço",
|
||||
"es": "Clave API de cuenta de servicio",
|
||||
"ar": "مفتاح API لحساب الخدمة",
|
||||
"fr": "Clé API du compte de service",
|
||||
"tr": "Hizmet Hesabı API Anahtarı",
|
||||
"de": "API-Schlüssel des Dienstkontos",
|
||||
"uk": "Ключ API облікового запису служби"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER": {
|
||||
"en": "sk-1234567890abcdef...",
|
||||
"ja": "sk-1234567890abcdef...",
|
||||
"zh-CN": "sk-1234567890abcdef...",
|
||||
"zh-TW": "sk-1234567890abcdef...",
|
||||
"ko-KR": "sk-1234567890abcdef...",
|
||||
"no": "sk-1234567890abcdef...",
|
||||
"it": "sk-1234567890abcdef...",
|
||||
"pt": "sk-1234567890abcdef...",
|
||||
"es": "sk-1234567890abcdef...",
|
||||
"ar": "sk-1234567890abcdef...",
|
||||
"fr": "sk-1234567890abcdef...",
|
||||
"tr": "sk-1234567890abcdef...",
|
||||
"de": "sk-1234567890abcdef...",
|
||||
"uk": "sk-1234567890abcdef..."
|
||||
},
|
||||
"PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL": {
|
||||
"en": "Connect",
|
||||
"ja": "接続する",
|
||||
"zh-CN": "连接",
|
||||
"zh-TW": "連接",
|
||||
"ko-KR": "연결하다",
|
||||
"no": "Koble til",
|
||||
"it": "Collegare",
|
||||
"pt": "Conectar",
|
||||
"es": "Conectar",
|
||||
"ar": "يتصل",
|
||||
"fr": "Connecter",
|
||||
"tr": "Bağlamak",
|
||||
"de": "Verbinden",
|
||||
"uk": "Підключитися"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL": {
|
||||
"en": "Active",
|
||||
"ja": "アクティブ",
|
||||
"zh-CN": "积极的",
|
||||
"zh-TW": "積極的",
|
||||
"ko-KR": "활동적인",
|
||||
"no": "Aktiv",
|
||||
"it": "Attiva",
|
||||
"pt": "Ativa",
|
||||
"es": "Activa",
|
||||
"ar": "نشيط",
|
||||
"fr": "Actif",
|
||||
"tr": "Aktif",
|
||||
"de": "Aktiv",
|
||||
"uk": "Активний"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION": {
|
||||
"en": "<b>Important:</b> Check the <a>documentation</a> for more information about configuring the workspace integration or updating an existing integration.",
|
||||
"ja": "<b>重要:</b> ワークスペース統合の設定や既存の統合の更新について詳しくは<a>ドキュメント</a>をご確認ください。",
|
||||
"zh-CN": "<b>重要提示:</b>有关配置工作区集成或更新现有集成的更多信息,请查看<a>文档</a>。",
|
||||
"zh-TW": "<b>重要提示:</b>有關配置工作區整合或更新現有整合的更多資訊,請查看<a>文件</a>。",
|
||||
"ko-KR": "<b>중요:</b> 작업공간 통합 구성이나 기존 통합 업데이트에 대한 자세한 내용은 <a>설명서</a>를 확인하세요.",
|
||||
"no": "<b>Viktig:</b> Se <a>dokumentasjonen</a> for mer informasjon om konfigurering av arbeidsområdeintegrasjon eller oppdatering av eksisterende integrasjon.",
|
||||
"it": "<b>Importante:</b> Consulta la <a>documentazione</a> per ulteriori informazioni sulla configurazione dell'integrazione dell'area di lavoro o sull'aggiornamento di un'integrazione esistente.",
|
||||
"pt": "<b>Importante:</b> Consulte a <a>documentação</a> para obter mais informações sobre como configurar a integração do workspace ou atualizar uma integração existente.",
|
||||
"es": "<b>Importante:</b> Consulte la <a>documentación</a> para obtener más información sobre la configuración de la integración del espacio de trabajo o la actualización de una integración existente.",
|
||||
"ar": "<b>هام:</b> راجع <a>الوثائق</a> لمزيد من المعلومات حول تكوين تكامل مساحة العمل أو تحديث تكامل موجود.",
|
||||
"fr": "<b>Important :</b> Consultez la <a>documentation</a> pour plus d'informations sur la configuration de l'intégration de l'espace de travail ou la mise à jour d'une intégration existante.",
|
||||
"tr": "<b>Önemli:</b> Çalışma alanı entegrasyonunu yapılandırma veya mevcut bir entegrasyonu güncelleme hakkında daha fazla bilgi için <a>belgelere</a> bakın.",
|
||||
"de": "<b>Wichtig:</b> Weitere Informationen zur Konfiguration der Arbeitsbereichsintegration oder zur Aktualisierung einer bestehenden Integration finden Sie in der <a>Dokumentation</a>.",
|
||||
"uk": "<b>Важливо:</b> Перегляньте <a>документацію</a> для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції."
|
||||
},
|
||||
"PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE": {
|
||||
"en": "Configure {{platform}} Integration",
|
||||
"ja": "{{platform}}統合を設定",
|
||||
"zh-CN": "配置{{platform}}集成",
|
||||
"zh-TW": "配置{{platform}}集成",
|
||||
"ko-KR": "{{platform}} 통합 구성",
|
||||
"no": "Konfigurer {{platform}}-integrasjon",
|
||||
"it": "Configura integrazione {{platform}}",
|
||||
"pt": "Configurar integração {{platform}}",
|
||||
"es": "Configurar integración de {{platform}}",
|
||||
"ar": "تكوين تكامل {{platform}}",
|
||||
"fr": "Configurer l'intégration {{platform}}",
|
||||
"tr": "{{platform}} Entegrasyonunu Yapılandır",
|
||||
"de": "{{platform}}-Integration konfigurieren",
|
||||
"uk": "Налаштувати інтеграцію {{platform}}"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION": {
|
||||
"en": "<b>Important: </b>Make sure the workspace integration for your target workspace is already configured. Check the <a>documentation</a> for more information.",
|
||||
"ja": "<b>重要: </b>対象のワークスペースのワークスペース統合がすでに設定されていることを確認してください。詳しくは<a>ドキュメント</a>をご覧ください。",
|
||||
"zh-CN": "<b>重要提示:</b>请确保目标工作区的工作区集成已配置完毕。查看<a>文档</a>了解更多信息。",
|
||||
"zh-TW": "<b>重要提示:</b>請確保目標工作區的工作區整合已設定完成。查看<a>文件</a>以了解更多資訊。",
|
||||
"ko-KR": "<b>중요:</b>대상 작업 공간에 대한 작업 공간 통합이 이미 구성되어 있는지 확인하세요. 자세한 내용은 <a>설명서</a>를 참조하세요.",
|
||||
"no": "<b>Viktig:</b>Sørg for at arbeidsområdeintegrasjonen for målarbeidsområdet ditt allerede er konfigurert. Se <a>dokumentasjonen</a> for mer informasjon.",
|
||||
"it": "<b>Importante: </b>Assicurati che l'integrazione dell'area di lavoro per l'area di lavoro di destinazione sia già configurata. Consulta la <a>documentazione</a> per ulteriori informazioni.",
|
||||
"pt": "<b>Importante:</b>Certifique-se de que a integração do workspace de destino já esteja configurada. Consulte a <a>documentação</a> para obter mais informações.",
|
||||
"es": "Importante: Asegúrate de que la integración del espacio de trabajo de destino ya esté configurada. Consulta la documentación para obtener más información.",
|
||||
"ar": "<b>هام: </b>تأكد من إعداد تكامل مساحة العمل لمساحة العمل المستهدفة. <a>راجع المستند لمزيد من المعلومات</a>.",
|
||||
"fr": "<b>Important :</b>Assurez-vous que l'intégration de l'espace de travail cible est déjà configurée. Consultez la <a>documentation</a> pour plus d'informations.",
|
||||
"tr": "<b>Önemli: </b>Hedef çalışma alanınız için çalışma alanı entegrasyonunun zaten yapılandırılmış olduğundan emin olun. Daha fazla bilgi için <a>belgelere</a> bakın.",
|
||||
"de": "<b>Wichtig:</b>Stellen Sie sicher, dass die Arbeitsbereichsintegration für Ihren Zielarbeitsbereich bereits konfiguriert ist. Weitere Informationen finden Sie in der <a>Dokumentation</a>.",
|
||||
"uk": "<b>Важливо: </b>Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте <a>документацію</a> для отримання додаткової інформації."
|
||||
},
|
||||
"PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR": {
|
||||
"en": "Workspace name can only contain letters, numbers, hyphens, and underscores",
|
||||
"ja": "ワークスペース名は文字、数字、ハイフン、アンダースコアのみ使用できます",
|
||||
"zh-CN": "工作区名称只能包含字母、数字、连字符和下划线",
|
||||
"zh-TW": "工作區名稱只能包含字母、數字、連字符和底線",
|
||||
"ko-KR": "작업공간 이름은 문자, 숫자, 하이픈, 밑줄만 포함할 수 있습니다",
|
||||
"no": "Arbeidsområdenavn kan bare inneholde bokstaver, tall, bindestrek og understrek",
|
||||
"it": "Il nome dell'area di lavoro può contenere solo lettere, numeri, trattini e trattini bassi",
|
||||
"pt": "O nome do workspace pode conter apenas letras, números, hífens e sublinhados",
|
||||
"es": "El nombre del espacio de trabajo solo puede contener letras, números, guiones y guiones bajos",
|
||||
"ar": "يمكن أن يحتوي اسم مساحة العمل على أحرف وأرقام وشرطات وشرطات سفلية فقط",
|
||||
"fr": "Le nom de l'espace de travail ne peut contenir que des lettres, des chiffres, des tirets et des traits de soulignement",
|
||||
"tr": "Çalışma alanı adı yalnızca harfler, sayılar, tire ve alt çizgi içerebilir",
|
||||
"de": "Der Arbeitsbereichsname darf nur Buchstaben, Zahlen, Bindestriche und Unterstriche enthalten",
|
||||
"uk": "Назва робочого простору може містити тільки літери, цифри, дефіси та підкреслення"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR": {
|
||||
"en": "Webhook secret cannot contain spaces",
|
||||
"ja": "Webhookシークレットにはスペースを含めることはできません",
|
||||
"zh-CN": "Webhook密钥不能包含空格",
|
||||
"zh-TW": "Webhook密鑰不能包含空格",
|
||||
"ko-KR": "웹훅 시크릿에는 공백을 포함할 수 없습니다",
|
||||
"no": "Webhook-hemmelighet kan ikke inneholde mellomrom",
|
||||
"it": "Il segreto del webhook non può contenere spazi",
|
||||
"pt": "O segredo do webhook não pode conter espaços",
|
||||
"es": "El secreto del webhook no puede contener espacios",
|
||||
"ar": "لا يمكن أن يحتوي سر الويب هوك على مسافات",
|
||||
"fr": "Le secret du webhook ne peut pas contenir d'espaces",
|
||||
"tr": "Webhook sırrı boşluk içeremez",
|
||||
"de": "Das Webhook-Geheimnis darf keine Leerzeichen enthalten",
|
||||
"uk": "Секрет веб-хука не може містити пробіли"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR": {
|
||||
"en": "Please enter a valid email address",
|
||||
"ja": "有効なメールアドレスを入力してください",
|
||||
"zh-CN": "请输入有效的电子邮件地址",
|
||||
"zh-TW": "請輸入有效的電子郵件地址",
|
||||
"ko-KR": "유효한 이메일 주소를 입력하세요",
|
||||
"no": "Vennligst skriv inn en gyldig e-postadresse",
|
||||
"it": "Inserisci un indirizzo email valido",
|
||||
"pt": "Por favor, insira um endereço de email válido",
|
||||
"es": "Por favor, introduce una dirección de correo electrónico válida",
|
||||
"ar": "يرجى إدخال عنوان بريد إلكتروني صحيح",
|
||||
"fr": "Veuillez saisir une adresse e-mail valide",
|
||||
"tr": "Lütfen geçerli bir e-posta adresi girin",
|
||||
"de": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||
"uk": "Будь ласка, введіть дійсну адресу електронної пошти"
|
||||
},
|
||||
"PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR": {
|
||||
"en": "API key cannot contain spaces",
|
||||
"ja": "APIキーにはスペースを含めることはできません",
|
||||
"zh-CN": "API密钥不能包含空格",
|
||||
"zh-TW": "API密鑰不能包含空格",
|
||||
"ko-KR": "API 키에는 공백을 포함할 수 없습니다",
|
||||
"no": "API-nøkkel kan ikke inneholde mellomrom",
|
||||
"it": "La chiave API non può contenere spazi",
|
||||
"pt": "A chave da API não pode conter espaços",
|
||||
"es": "La clave API no puede contener espacios",
|
||||
"ar": "لا يمكن أن يحتوي مفتاح API على مسافات",
|
||||
"fr": "La clé API ne peut pas contenir d'espaces",
|
||||
"tr": "API anahtarı boşluk içeremez",
|
||||
"de": "Der API-Schlüssel darf keine Leerzeichen enthalten",
|
||||
"uk": "Ключ API не може містити пробіли"
|
||||
},
|
||||
"MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT": {
|
||||
"en": "Error loading microagent content.",
|
||||
"ja": "マイクロエージェントのコンテンツの読み込み中にエラーが発生しました。",
|
||||
"zh-CN": "加载微代理内容时出错。",
|
||||
"zh-TW": "載入微代理內容時發生錯誤。",
|
||||
"ko-KR": "마이크로에이전트 콘텐츠를 불러오는 중 오류가 발생했습니다.",
|
||||
"no": "Feil ved lasting av mikroagent-innhold.",
|
||||
"it": "Errore durante il caricamento del contenuto del microagente.",
|
||||
"pt": "Erro ao carregar o conteúdo do microagente.",
|
||||
"es": "Error al cargar el contenido del microagente.",
|
||||
"ar": "حدث خطأ أثناء تحميل محتوى الوكيل الدقيق.",
|
||||
"fr": "Erreur lors du chargement du contenu du microagent.",
|
||||
"tr": "Mikro ajan içeriği yüklenirken hata oluştu.",
|
||||
"de": "Fehler beim Laden des Microagent-Inhalts.",
|
||||
"uk": "Помилка під час завантаження вмісту мікроагента."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,9 @@ export const handlers = [
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: mockSaas,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
// Uncomment the following to test the maintenance banner
|
||||
// MAINTENANCE: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Controls } from "#/components/features/controls/controls";
|
||||
import { clearTerminal } from "#/state/command-slice";
|
||||
import { useEffectOnce } from "#/hooks/use-effect-once";
|
||||
import { clearJupyter } from "#/state/jupyter-slice";
|
||||
|
||||
import { useBatchFeedback } from "#/hooks/query/use-batch-feedback";
|
||||
import { ChatInterface } from "../components/features/chat/chat-interface";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { EventHandler } from "../wrapper/event-handler";
|
||||
@@ -37,6 +37,9 @@ function AppContent() {
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
// Fetch batch feedback data when conversation is loaded
|
||||
useBatchFeedback();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"
|
||||
import { GitSettingInputsSkeleton } from "#/components/features/settings/git-settings/github-settings-inputs-skeleton";
|
||||
import { useAddGitProviders } from "#/hooks/mutation/use-add-git-providers";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { ProjectManagementIntegration } from "#/components/features/settings/project-management/project-management-integration";
|
||||
|
||||
function GitSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -110,6 +111,10 @@ function GitSettingsScreen() {
|
||||
!gitlabHostInputHasValue &&
|
||||
!bitbucketHostInputHasValue;
|
||||
const shouldRenderExternalConfigureButtons = isSaas && config.APP_SLUG;
|
||||
const shouldRenderProjectManagementIntegrations =
|
||||
config?.FEATURE_FLAGS?.ENABLE_JIRA ||
|
||||
config?.FEATURE_FLAGS?.ENABLE_JIRA_DC ||
|
||||
config?.FEATURE_FLAGS?.ENABLE_LINEAR;
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -118,13 +123,35 @@ function GitSettingsScreen() {
|
||||
className="flex flex-col h-full justify-between"
|
||||
>
|
||||
{!isLoading && (
|
||||
<div className="p-9 flex flex-col gap-12">
|
||||
<div className="p-9 flex flex-col">
|
||||
{shouldRenderExternalConfigureButtons && !isLoading && (
|
||||
<ConfigureGitHubRepositoriesAnchor slug={config.APP_SLUG!} />
|
||||
<>
|
||||
<div className="pb-1 flex flex-col">
|
||||
<h3 className="text-xl font-medium text-white">
|
||||
{t(I18nKey.SETTINGS$GITHUB)}
|
||||
</h3>
|
||||
<ConfigureGitHubRepositoriesAnchor slug={config.APP_SLUG!} />
|
||||
</div>
|
||||
<div className="w-1/2 border-b border-gray-200" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{shouldRenderExternalConfigureButtons && !isLoading && (
|
||||
<InstallSlackAppAnchor />
|
||||
<>
|
||||
<div className="pb-1 mt-6 flex flex-col">
|
||||
<h3 className="text-xl font-medium text-white">
|
||||
{t(I18nKey.SETTINGS$SLACK)}
|
||||
</h3>
|
||||
<InstallSlackAppAnchor />
|
||||
</div>
|
||||
<div className="w-1/2 border-b border-gray-200" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{shouldRenderProjectManagementIntegrations && !isLoading && (
|
||||
<div className="mt-6">
|
||||
<ProjectManagementIntegration />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSaas && (
|
||||
|
||||
@@ -64,7 +64,6 @@ export interface FinishAction extends OpenHandsActionEvent<"finish"> {
|
||||
source: "agent";
|
||||
args: {
|
||||
final_thought: string;
|
||||
task_completed: "success" | "failure" | "partial";
|
||||
outputs: Record<string, unknown>;
|
||||
thought: string;
|
||||
};
|
||||
|
||||
@@ -4,11 +4,6 @@ export type TabType = "personal" | "repositories" | "organizations";
|
||||
|
||||
export interface RepositoryMicroagent {
|
||||
name: string;
|
||||
type: "repo" | "knowledge";
|
||||
content: string;
|
||||
triggers: string[];
|
||||
inputs: string[];
|
||||
tools: string[];
|
||||
created_at: string;
|
||||
git_provider: string;
|
||||
path: string;
|
||||
|
||||
@@ -2,6 +2,7 @@ export const ProviderOptions = {
|
||||
github: "github",
|
||||
gitlab: "gitlab",
|
||||
bitbucket: "bitbucket",
|
||||
enterprise_sso: "enterprise_sso",
|
||||
} as const;
|
||||
|
||||
export type Provider = keyof typeof ProviderOptions;
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum LoginMethod {
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
BITBUCKET = "bitbucket",
|
||||
ENTERPRISE_SSO = "enterprise_sso",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -207,3 +207,22 @@ export const constructMicroagentUrl = (
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract repository owner, repo name, and file path from repository and microagent data
|
||||
* @param selectedRepository The selected repository object with full_name property
|
||||
* @param microagent The microagent object with path property
|
||||
* @returns Object containing owner, repo, and filePath
|
||||
*
|
||||
* @example
|
||||
* const { owner, repo, filePath } = extractRepositoryInfo(selectedRepository, microagent);
|
||||
*/
|
||||
export const extractRepositoryInfo = (
|
||||
selectedRepository: { full_name?: string } | null | undefined,
|
||||
microagent: { path?: string } | null | undefined,
|
||||
) => {
|
||||
const [owner, repo] = selectedRepository?.full_name?.split("/") || [];
|
||||
const filePath = microagent?.path || "";
|
||||
|
||||
return { owner, repo, filePath };
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export const VERIFIED_MODELS = [
|
||||
"devstral-small-2507",
|
||||
"devstral-medium-2507",
|
||||
"kimi-k2-0711-preview",
|
||||
"qwen3-coder-480b",
|
||||
];
|
||||
|
||||
// LiteLLM does not return OpenAI models with the provider, so we list them here to set them ourselves for consistency
|
||||
@@ -68,6 +69,7 @@ export const VERIFIED_OPENHANDS_MODELS = [
|
||||
"devstral-medium-2507",
|
||||
"devstral-small-2505",
|
||||
"kimi-k2-0711-preview",
|
||||
"qwen3-coder-480b",
|
||||
];
|
||||
|
||||
// Default model for OpenHands provider
|
||||
|
||||
@@ -8,35 +8,11 @@ triggers:
|
||||
- container
|
||||
---
|
||||
|
||||
# Docker Installation and Usage Guide
|
||||
|
||||
## Installation on Debian/Ubuntu Systems
|
||||
|
||||
To install Docker on a Debian/Ubuntu system, follow these steps:
|
||||
|
||||
```bash
|
||||
# Update package index
|
||||
sudo apt-get update
|
||||
|
||||
# Install prerequisites
|
||||
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
|
||||
|
||||
# Add Docker's official GPG key
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
|
||||
# Set up the stable repository
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
# Update package index again
|
||||
sudo apt-get update
|
||||
|
||||
# Install Docker Engine
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
```
|
||||
# Docker Usage Guide
|
||||
|
||||
## Starting Docker in Container Environments
|
||||
|
||||
If you're in a container environment without systemd (like this workspace), start Docker with:
|
||||
Please check if docker is already installed. If so, to start Docker in a container environment:
|
||||
|
||||
```bash
|
||||
# Start Docker daemon in the background
|
||||
|
||||
@@ -161,6 +161,13 @@ class CodeActAgent(Agent):
|
||||
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
- AgentFinishAction() - end the interaction
|
||||
- CondensationAction(...) - condense conversation history by forgetting specified events and optionally providing a summary
|
||||
- FileReadAction(path, ...) - read file content from specified path
|
||||
- FileEditAction(path, ...) - edit file using LLM-based (deprecated) or ACI-based editing
|
||||
- AgentThinkAction(thought) - log agent's thought/reasoning process
|
||||
- CondensationRequestAction() - request condensation of conversation history
|
||||
- BrowseInteractiveAction(browser_actions) - interact with browser using specified actions
|
||||
- MCPAction(name, arguments) - interact with MCP server tools
|
||||
"""
|
||||
# Continue with pending actions if any
|
||||
if self.pending_actions:
|
||||
@@ -193,7 +200,11 @@ class CodeActAgent(Agent):
|
||||
'messages': self.llm.format_messages_for_llm(messages),
|
||||
}
|
||||
params['tools'] = check_tools(self.tools, self.llm.config)
|
||||
params['extra_body'] = {'metadata': state.to_llm_metadata(agent_name=self.name)}
|
||||
params['extra_body'] = {
|
||||
'metadata': state.to_llm_metadata(
|
||||
model_name=self.llm.config.model, agent_name=self.name
|
||||
)
|
||||
}
|
||||
response = self.llm.completion(**params)
|
||||
logger.debug(f'Response from LLM: {response}')
|
||||
actions = self.response_to_actions(response)
|
||||
|
||||
@@ -123,7 +123,6 @@ def response_to_actions(
|
||||
elif tool_call.function.name == FinishTool['function']['name']:
|
||||
action = AgentFinishAction(
|
||||
final_thought=arguments.get('message', ''),
|
||||
task_completed=arguments.get('task_completed', None),
|
||||
)
|
||||
|
||||
# ================================================
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{% if repository_info %}
|
||||
<REPOSITORY_INFO>
|
||||
At the user's request, repository {{ repository_info.repo_name }} has been cloned to {{ repository_info.repo_directory }} in the current working directory.
|
||||
{% if repository_info.branch_name %}The repository has been checked out to branch "{{ repository_info.branch_name }}".
|
||||
|
||||
IMPORTANT: You should work within the current branch "{{ repository_info.branch_name }}" unless
|
||||
1. the user explicitly instructs otherwise
|
||||
2. if the current branch is "main", "master", or another default branch where direct pushes may be unsafe
|
||||
{% endif %}
|
||||
</REPOSITORY_INFO>
|
||||
{% endif %}
|
||||
{% if repository_instructions -%}
|
||||
|
||||
@@ -31,7 +31,7 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
</CODE_QUALITY>
|
||||
|
||||
<VERSION_CONTROL>
|
||||
* When committing changes, you MUST use the `--author` flag to set the author to `"openhands <openhands@all-hands.dev>"`. For example: `git commit --author="openhands <openhands@all-hands.dev>" -m "Fix bug"`. This ensures all commits are attributed to the OpenHands agent, regardless of the local git config.
|
||||
* If there are existing git user credentials already configured, use them and add Co-authored-by: openhands <openhands@all-hands.dev> to any commits messages you make. if a git config doesn't exist use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
|
||||
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
|
||||
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
|
||||
|
||||
@@ -25,7 +25,7 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
</CODE_QUALITY>
|
||||
|
||||
<VERSION_CONTROL>
|
||||
* When committing changes, you MUST use the `--author` flag to set the author to `"openhands <openhands@all-hands.dev>"`. For example: `git commit --author="openhands <openhands@all-hands.dev>" -m "Fix bug"`. This ensures all commits are attributed to the OpenHands agent, regardless of the local git config.
|
||||
* If there are existing git user credentials already configured, use them and add Co-authored-by: openhands <openhands@all-hands.dev> to any commits messages you make. if a git config doesn't exist use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
|
||||
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
|
||||
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
|
||||
|
||||
@@ -25,7 +25,7 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
</CODE_QUALITY>
|
||||
|
||||
<VERSION_CONTROL>
|
||||
* When committing changes, you MUST use the `--author` flag to set the author to `"openhands <openhands@all-hands.dev>"`. For example: `git commit --author="openhands <openhands@all-hands.dev>" -m "Fix bug"`. This ensures all commits are attributed to the OpenHands agent, regardless of the local git config.
|
||||
* If there are existing git user credentials already configured, use them and add Co-authored-by: openhands <openhands@all-hands.dev> to any commits messages you make. if a git config doesn't exist use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
|
||||
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
|
||||
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
|
||||
|
||||
@@ -13,8 +13,6 @@ The message should include:
|
||||
- Any next steps for the user
|
||||
- Explanation if you're unable to complete the task
|
||||
- Any follow-up questions if more information is needed
|
||||
|
||||
The task_completed field should be set to True if you believed you have completed the task, and False otherwise.
|
||||
"""
|
||||
|
||||
FinishTool = ChatCompletionToolParam(
|
||||
@@ -24,17 +22,12 @@ FinishTool = ChatCompletionToolParam(
|
||||
description=_FINISH_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'required': ['message', 'task_completed'],
|
||||
'required': ['message'],
|
||||
'properties': {
|
||||
'message': {
|
||||
'type': 'string',
|
||||
'description': 'Final message to send to the user',
|
||||
},
|
||||
'task_completed': {
|
||||
'type': 'string',
|
||||
'enum': ['true', 'false', 'partial'],
|
||||
'description': 'Whether you have completed the task.',
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
@@ -81,7 +81,6 @@ def response_to_actions(
|
||||
elif tool_call.function.name == FinishTool['function']['name']:
|
||||
action = AgentFinishAction(
|
||||
final_thought=arguments.get('message', ''),
|
||||
task_completed=arguments.get('task_completed', None),
|
||||
)
|
||||
else:
|
||||
raise FunctionCallNotExistsError(
|
||||
|
||||
@@ -141,7 +141,6 @@ def response_to_actions(
|
||||
if tool_call.function.name == FinishTool['function']['name']:
|
||||
action = AgentFinishAction(
|
||||
final_thought=arguments.get('message', ''),
|
||||
task_completed=arguments.get('task_completed', None),
|
||||
)
|
||||
|
||||
# ================================================
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{% if repository_info %}
|
||||
<REPOSITORY_INFO>
|
||||
At the user's request, repository {{ repository_info.repo_name }} has been cloned to the current working directory {{ repository_info.repo_directory }}.
|
||||
{% if repository_info.branch_name %}The repository has been checked out to branch "{{ repository_info.branch_name }}".
|
||||
|
||||
IMPORTANT: You should work within the current branch "{{ repository_info.branch_name }}" unless
|
||||
1. the user explicitly instructs otherwise
|
||||
2. if the current branch is "main", "master", or another default branch where direct pushes may be unsafe
|
||||
{% endif %}
|
||||
</REPOSITORY_INFO>
|
||||
{% endif %}
|
||||
{% if repository_instructions -%}
|
||||
|
||||
@@ -15,6 +15,7 @@ from openhands.cli.settings import (
|
||||
display_settings,
|
||||
modify_llm_settings_advanced,
|
||||
modify_llm_settings_basic,
|
||||
modify_search_api_settings,
|
||||
)
|
||||
from openhands.cli.tui import (
|
||||
COLOR_GREY,
|
||||
@@ -126,6 +127,7 @@ async def handle_commands(
|
||||
config: OpenHandsConfig,
|
||||
current_dir: str,
|
||||
settings_store: FileSettingsStore,
|
||||
agent_state: str,
|
||||
) -> tuple[bool, bool, bool, ExitReason]:
|
||||
close_repl = False
|
||||
reload_microagents = False
|
||||
@@ -158,7 +160,9 @@ async def handle_commands(
|
||||
elif command == '/settings':
|
||||
await handle_settings_command(config, settings_store)
|
||||
elif command == '/resume':
|
||||
close_repl, new_session_requested = await handle_resume_command(event_stream)
|
||||
close_repl, new_session_requested = await handle_resume_command(
|
||||
event_stream, agent_state
|
||||
)
|
||||
elif command == '/mcp':
|
||||
await handle_mcp_command(config)
|
||||
else:
|
||||
@@ -271,8 +275,9 @@ async def handle_settings_command(
|
||||
config,
|
||||
'\nWhich settings would you like to modify?',
|
||||
[
|
||||
'Basic',
|
||||
'Advanced',
|
||||
'LLM (Basic)',
|
||||
'LLM (Advanced)',
|
||||
'Search API (Optional)',
|
||||
'Go back',
|
||||
],
|
||||
)
|
||||
@@ -281,6 +286,8 @@ async def handle_settings_command(
|
||||
await modify_llm_settings_basic(config, settings_store)
|
||||
elif modify_settings == 1:
|
||||
await modify_llm_settings_advanced(config, settings_store)
|
||||
elif modify_settings == 2:
|
||||
await modify_search_api_settings(config, settings_store)
|
||||
|
||||
|
||||
# FIXME: Currently there's an issue with the actual 'resume' behavior.
|
||||
@@ -288,10 +295,20 @@ async def handle_settings_command(
|
||||
# This is a workaround to handle the resume command for the time being. Replace user message with the state change event once the issue is fixed.
|
||||
async def handle_resume_command(
|
||||
event_stream: EventStream,
|
||||
agent_state: str,
|
||||
) -> tuple[bool, bool]:
|
||||
close_repl = True
|
||||
new_session_requested = False
|
||||
|
||||
if agent_state != AgentState.PAUSED:
|
||||
close_repl = False
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'<ansired>Error: Agent is not paused. /resume command is only available when agent is paused.</ansired>'
|
||||
)
|
||||
)
|
||||
return close_repl, new_session_requested
|
||||
|
||||
event_stream.add_event(
|
||||
MessageAction(content='continue'),
|
||||
EventSource.USER,
|
||||
|
||||
@@ -49,7 +49,9 @@ from openhands.core.config import (
|
||||
setup_config_from_args,
|
||||
)
|
||||
from openhands.core.config.condenser_config import NoOpCondenserConfig
|
||||
from openhands.core.config.mcp_config import OpenHandsMCPConfigImpl
|
||||
from openhands.core.config.mcp_config import (
|
||||
OpenHandsMCPConfigImpl,
|
||||
)
|
||||
from openhands.core.config.utils import finalize_config
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.loop import run_agent_until_done
|
||||
@@ -188,6 +190,7 @@ async def run_session(
|
||||
config,
|
||||
current_dir,
|
||||
settings_store,
|
||||
agent_state,
|
||||
)
|
||||
|
||||
if close_repl:
|
||||
@@ -417,6 +420,19 @@ async def run_setup_flow(config: OpenHandsConfig, settings_store: FileSettingsSt
|
||||
# Use the existing settings modification function for basic setup
|
||||
await modify_llm_settings_basic(config, settings_store)
|
||||
|
||||
# Ask if user wants to configure search API settings
|
||||
print_formatted_text('')
|
||||
setup_search = cli_confirm(
|
||||
config,
|
||||
'Would you like to configure Search API settings (optional)?',
|
||||
['Yes', 'No'],
|
||||
)
|
||||
|
||||
if setup_search == 0: # Yes
|
||||
from openhands.cli.settings import modify_search_api_settings
|
||||
|
||||
await modify_search_api_settings(config, settings_store)
|
||||
|
||||
|
||||
def run_alias_setup_flow(config: OpenHandsConfig) -> None:
|
||||
"""Run the alias setup flow to configure shell aliases.
|
||||
@@ -590,6 +606,11 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
|
||||
settings.confirmation_mode if settings.confirmation_mode else False
|
||||
)
|
||||
|
||||
# Load search API key from settings if available and not already set from config.toml
|
||||
if settings.search_api_key and not config.search_api_key:
|
||||
config.search_api_key = settings.search_api_key
|
||||
logger.debug('Using search API key from settings.json')
|
||||
|
||||
if settings.enable_default_condenser:
|
||||
# TODO: Make this generic?
|
||||
llm_config = config.get_llm_config()
|
||||
|
||||
@@ -23,7 +23,10 @@ from openhands.cli.utils import (
|
||||
)
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.core.config.condenser_config import NoOpCondenserConfig
|
||||
from openhands.core.config.condenser_config import (
|
||||
CondenserPipelineConfig,
|
||||
ConversationWindowCondenserConfig,
|
||||
)
|
||||
from openhands.core.config.utils import OH_DEFAULT_AGENT
|
||||
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
|
||||
LLMSummarizingCondenserConfig,
|
||||
@@ -74,6 +77,10 @@ def display_settings(config: OpenHandsConfig) -> None:
|
||||
' Memory Condensation',
|
||||
'Enabled' if config.enable_default_condenser else 'Disabled',
|
||||
),
|
||||
(
|
||||
' Search API Key',
|
||||
'********' if config.search_api_key else 'Not Set',
|
||||
),
|
||||
(
|
||||
' Configuration File',
|
||||
str(Path(config.file_store_path) / 'settings.json'),
|
||||
@@ -268,14 +275,15 @@ async def modify_llm_settings_basic(
|
||||
|
||||
# For OpenHands provider, directly show all verified models without the "use default" option
|
||||
if provider == 'openhands':
|
||||
print_formatted_text(HTML('\n<grey>Available OpenHands models:</grey>'))
|
||||
|
||||
# Create a list of models for the cli_confirm function
|
||||
model_choices = VERIFIED_OPENHANDS_MODELS
|
||||
|
||||
model_choice = cli_confirm(
|
||||
config,
|
||||
'(Step 2/3) Select LLM Model:',
|
||||
(
|
||||
'(Step 2/3) Select Available OpenHands Model:\n'
|
||||
+ 'LLM usage is billed at the providers’ rates with no markup. Details: https://docs.all-hands.dev/usage/llms/openhands-llms'
|
||||
),
|
||||
model_choices,
|
||||
)
|
||||
|
||||
@@ -467,12 +475,21 @@ async def modify_llm_settings_advanced(
|
||||
|
||||
agent_config = config.get_agent_config(config.default_agent)
|
||||
if enable_memory_condensation:
|
||||
agent_config.condenser = LLMSummarizingCondenserConfig(
|
||||
llm_config=llm_config,
|
||||
type='llm',
|
||||
agent_config.condenser = CondenserPipelineConfig(
|
||||
type='pipeline',
|
||||
condensers=[
|
||||
ConversationWindowCondenserConfig(type='conversation_window'),
|
||||
# Use LLMSummarizingCondenserConfig with the custom llm_config
|
||||
LLMSummarizingCondenserConfig(
|
||||
llm_config=llm_config, type='llm', keep_first=4, max_size=120
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
else:
|
||||
agent_config.condenser = NoOpCondenserConfig(type='noop')
|
||||
agent_config.condenser = ConversationWindowCondenserConfig(
|
||||
type='conversation_window'
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
|
||||
settings = await settings_store.load()
|
||||
@@ -487,3 +504,75 @@ async def modify_llm_settings_advanced(
|
||||
settings.enable_default_condenser = enable_memory_condensation
|
||||
|
||||
await settings_store.store(settings)
|
||||
|
||||
|
||||
async def modify_search_api_settings(
|
||||
config: OpenHandsConfig, settings_store: FileSettingsStore
|
||||
) -> None:
|
||||
"""Modify search API settings."""
|
||||
session = PromptSession(key_bindings=kb_cancel())
|
||||
|
||||
search_api_key = None
|
||||
|
||||
try:
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'\n<grey>Configure Search API Key for enhanced search capabilities.</grey>'
|
||||
)
|
||||
)
|
||||
print_formatted_text(
|
||||
HTML('<grey>You can get a Tavily API key from: https://tavily.com/</grey>')
|
||||
)
|
||||
print_formatted_text('')
|
||||
|
||||
# Show current status
|
||||
current_key_status = '********' if config.search_api_key else 'Not Set'
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
f'<grey>Current Search API Key: </grey><green>{current_key_status}</green>'
|
||||
)
|
||||
)
|
||||
print_formatted_text('')
|
||||
|
||||
# Ask if user wants to modify
|
||||
modify_key = cli_confirm(
|
||||
config,
|
||||
'Do you want to modify the Search API Key?',
|
||||
['Set/Update API Key', 'Remove API Key', 'Keep current setting'],
|
||||
)
|
||||
|
||||
if modify_key == 0: # Set/Update API Key
|
||||
search_api_key = await get_validated_input(
|
||||
session,
|
||||
'Enter Tavily Search API Key. You can get it from https://www.tavily.com/ (starts with tvly-, CTRL-c to cancel): ',
|
||||
validator=lambda x: x.startswith('tvly-') if x.strip() else False,
|
||||
error_message='Search API Key must start with "tvly-"',
|
||||
)
|
||||
elif modify_key == 1: # Remove API Key
|
||||
search_api_key = '' # Empty string to remove the key
|
||||
else: # Keep current setting
|
||||
return
|
||||
|
||||
except (
|
||||
UserCancelledError,
|
||||
KeyboardInterrupt,
|
||||
EOFError,
|
||||
):
|
||||
return # Return on exception
|
||||
|
||||
save_settings = save_settings_confirmation(config)
|
||||
|
||||
if not save_settings:
|
||||
return
|
||||
|
||||
# Update config
|
||||
config.search_api_key = SecretStr(search_api_key) if search_api_key else None
|
||||
|
||||
# Update settings store
|
||||
settings = await settings_store.load()
|
||||
if not settings:
|
||||
settings = Settings()
|
||||
|
||||
settings.search_api_key = SecretStr(search_api_key) if search_api_key else None
|
||||
|
||||
await settings_store.store(settings)
|
||||
|
||||
@@ -190,6 +190,7 @@ VERIFIED_OPENHANDS_MODELS = [
|
||||
'o4-mini',
|
||||
'gemini-2.5-pro',
|
||||
'kimi-k2-0711-preview',
|
||||
'qwen3-coder-480b',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -657,6 +657,7 @@ class AgentController:
|
||||
# Take a snapshot of the current metrics before starting the delegate
|
||||
state = State(
|
||||
session_id=self.id.removesuffix('-delegate'),
|
||||
user_id=self.user_id,
|
||||
inputs=action.inputs or {},
|
||||
iteration_flag=self.state.iteration_flag,
|
||||
budget_flag=self.state.budget_flag,
|
||||
|
||||
@@ -79,6 +79,7 @@ class State:
|
||||
"""
|
||||
|
||||
session_id: str = ''
|
||||
user_id: str | None = None
|
||||
iteration_flag: IterationControlFlag = field(
|
||||
default_factory=lambda: IterationControlFlag(
|
||||
limit_increase_amount=100, current_value=0, max_value=100
|
||||
@@ -265,16 +266,19 @@ class State:
|
||||
return event
|
||||
return None
|
||||
|
||||
def to_llm_metadata(self, agent_name: str) -> dict:
|
||||
return {
|
||||
def to_llm_metadata(self, model_name: str, agent_name: str) -> dict:
|
||||
metadata = {
|
||||
'session_id': self.session_id,
|
||||
'trace_version': openhands.__version__,
|
||||
'trace_user_id': self.user_id,
|
||||
'tags': [
|
||||
f'model:{model_name}',
|
||||
f'agent:{agent_name}',
|
||||
f'web_host:{os.environ.get("WEB_HOST", "unspecified")}',
|
||||
f'openhands_version:{openhands.__version__}',
|
||||
],
|
||||
}
|
||||
return metadata
|
||||
|
||||
def get_local_step(self):
|
||||
if not self.parent_iteration:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user