Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c556d6396 | |||
| 8bb5aa21b9 | |||
| 08096db29f | |||
| b2b6ddf90c | |||
| 87fe36d811 | |||
| 39d255d313 | |||
| e334b67f21 | |||
| d5c02bf87b | |||
| 14a4664fe8 | |||
| 3a7df33acf | |||
| 69fddecc7f | |||
| 3afe5ccee5 | |||
| 3d5a8dcf5a | |||
| 2ee1abe22c | |||
| 148940f553 | |||
| 1f09296136 | |||
| 70e5d12ba9 | |||
| bcb3160d95 | |||
| 174c691744 | |||
| af34d446e9 | |||
| 6604924f76 | |||
| b2def1e438 | |||
| 2b8e47aca9 | |||
| dba8b28824 |
@@ -59,7 +59,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -86,35 +85,14 @@ jobs:
|
||||
run: |
|
||||
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --push
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: aquasecurity/trivy-action@master
|
||||
# This only reports, does not fail the build on CVE.
|
||||
with:
|
||||
image-ref: ghcr.io/${{ env.REPO_OWNER }}/openhands:${{ env.RELEVANT_SHA }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
timeout: '10m'
|
||||
scanners: 'vuln' # Only scan vulnerabilities, not secrets/config
|
||||
severity: 'CRITICAL,HIGH,MEDIUM' # Skip LOW severity
|
||||
|
||||
- name: Upload Trivy results to GitHub Security tab
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
category: 'security/container-openhands-${{ matrix.base_image.tag }}'
|
||||
sha: "${{ env.RELEVANT_SHA }}"
|
||||
ref: refs/heads/${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
# Builds the runtime Docker images
|
||||
ghcr_build_runtime:
|
||||
name: Build Runtime Image
|
||||
name: Build Image
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
needs: define-matrix
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -188,41 +166,12 @@ jobs:
|
||||
name: runtime-src-${{ matrix.base_image.tag }}
|
||||
path: containers/runtime
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: aquasecurity/trivy-action@master
|
||||
# This only reports, does not fail the build on CVE.
|
||||
with:
|
||||
image-ref: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results-raw.sarif'
|
||||
timeout: '10m'
|
||||
scanners: 'vuln' # Only scan vulnerabilities, not secrets/config
|
||||
severity: 'CRITICAL,HIGH,MEDIUM' # Skip LOW severity
|
||||
- name: Customize SARIF with image flavor
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
# Modify the tool name to include the image flavor
|
||||
jq --arg flavor "${{ matrix.base_image.tag }}" \
|
||||
'.runs[0].tool.driver.name = "Trivy (" + $flavor + ")"' \
|
||||
trivy-results-raw.sarif > trivy-results.sarif
|
||||
|
||||
echo "Modified tool name to: $(jq -r '.runs[0].tool.driver.name' trivy-results.sarif)"
|
||||
|
||||
- name: Upload Trivy results to GitHub Security tab
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
category: 'security/container-runtime-${{ matrix.base_image.tag }}'
|
||||
|
||||
ghcr_build_enterprise:
|
||||
name: Build Enterprise Image
|
||||
name: Push Enterprise Image
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
needs: [define-matrix, ghcr_build_app]
|
||||
# Do not build enterprise in forks
|
||||
if: github.event.pull_request.head.repo.fork != true
|
||||
@@ -262,16 +211,14 @@ jobs:
|
||||
latest=auto
|
||||
prefix=
|
||||
suffix=
|
||||
env:
|
||||
DOCKER_METADATA_PR_HEAD_SHA: true
|
||||
- name: Determine app image tag
|
||||
shell: bash
|
||||
run: |
|
||||
# Duplicated with build.sh
|
||||
sanitized_ref_name=$(echo "${{github.event.pull_request.head.sha}}" | sed 's/[^a-zA-Z0-9.-]\+/-/g')
|
||||
sanitized_ref_name=$(echo "$GITHUB_REF_NAME" | sed 's/[^a-zA-Z0-9.-]\+/-/g')
|
||||
OPENHANDS_BUILD_VERSION=$sanitized_ref_name
|
||||
sanitized_ref_name=$(echo "$sanitized_ref_name" | tr '[:upper:]' '[:lower:]') # lower case is required in tagging
|
||||
echo "OPENHANDS_BUILD_VERSION=${sanitized_ref_name}" >> $GITHUB_ENV
|
||||
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
echo "OPENHANDS_DOCKER_TAG=${sanitized_ref_name}" >> $GITHUB_ENV
|
||||
- name: Build and push Docker image
|
||||
uses: useblacksmith/build-push-action@v1
|
||||
with:
|
||||
@@ -281,30 +228,12 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
OPENHANDS_VERSION=${{ env.OPENHANDS_BUILD_VERSION }}
|
||||
OPENHANDS_VERSION=${{ env.OPENHANDS_DOCKER_TAG }}
|
||||
platforms: linux/amd64
|
||||
# Add build provenance
|
||||
provenance: true
|
||||
# Add build attestations for better security
|
||||
sbom: true
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
if: github.event_name != 'pull_request'
|
||||
# This only reports, does not fail the build on CVE.
|
||||
with:
|
||||
image-ref: "ghcr.io/${{ env.REPO_OWNER }}/enterprise-server:sha-${{ env.RELEVANT_SHA }}"
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
timeout: '10m'
|
||||
scanners: 'vuln' # Only scan vulnerabilities, not secrets/config
|
||||
severity: 'CRITICAL,HIGH,MEDIUM' # Skip LOW severity
|
||||
|
||||
- name: Upload Trivy results to GitHub Security tab
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
category: 'security/container-enterprise-server'
|
||||
|
||||
enterprise-preview:
|
||||
name: Enterprise preview
|
||||
|
||||
@@ -19,16 +19,12 @@ jobs:
|
||||
# Run python tests on Linux
|
||||
test-on-linux:
|
||||
name: Python Tests on Linux
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
env:
|
||||
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
permissions:
|
||||
# For coverage comment and python-coverage-comment-action branch
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -52,21 +48,10 @@ jobs:
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Unit Tests
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -s ./tests/unit --cov=openhands --cov-branch
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.${{ matrix.python_version }}"
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv ./tests/unit
|
||||
- name: Run Runtime Tests with CLIRuntime
|
||||
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -s tests/runtime/test_bash.py --cov=openhands --cov-branch
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
|
||||
- name: Store coverage file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-openhands
|
||||
path: |
|
||||
.coverage.${{ matrix.python_version }}
|
||||
.coverage.runtime.${{ matrix.python_version }}
|
||||
include-hidden-files: true
|
||||
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
|
||||
|
||||
# Run specific Windows python tests
|
||||
test-on-windows:
|
||||
name: Python Tests on Windows
|
||||
@@ -100,7 +85,7 @@ jobs:
|
||||
DEBUG: "1"
|
||||
test-enterprise:
|
||||
name: Enterprise Python Unit Tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
@@ -117,37 +102,5 @@ jobs:
|
||||
working-directory: ./enterprise
|
||||
run: poetry install --with dev,test
|
||||
- name: Run Unit Tests
|
||||
# Use base working directory for coverage paths to line up.
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run --project=enterprise pytest --forked -n auto -s -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./enterprise/tests/unit --cov=enterprise --cov-branch
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.enterprise.${{ matrix.python_version }}"
|
||||
- name: Store coverage file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-enterprise
|
||||
path: ".coverage.enterprise.${{ matrix.python_version }}"
|
||||
include-hidden-files: true
|
||||
coverage-comment:
|
||||
name: Coverage Comment
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-on-linux, test-enterprise]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
id: download
|
||||
with:
|
||||
pattern: coverage-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Coverage comment
|
||||
id: coverage_comment
|
||||
uses: py-cov-action/python-coverage-comment-action@v3
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
MERGE_COVERAGE_FILES: true
|
||||
working-directory: ./enterprise
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./tests/unit
|
||||
|
||||
@@ -489,47 +489,6 @@ type = "noop"
|
||||
# Run the runtime sandbox container in privileged mode for use with docker-in-docker
|
||||
#privileged = false
|
||||
|
||||
#################################### MCP #####################################
|
||||
# Configuration for Model Context Protocol (MCP) servers
|
||||
# MCP allows OpenHands to communicate with external tool servers
|
||||
##############################################################################
|
||||
[mcp]
|
||||
# SSE servers - Server-Sent Events transport (legacy)
|
||||
#sse_servers = [
|
||||
# # Basic SSE server with just a URL
|
||||
# "http://localhost:8080/mcp/sse",
|
||||
#
|
||||
# # SSE server with authentication
|
||||
# {url = "https://api.example.com/mcp/sse", api_key = "your-api-key"}
|
||||
#]
|
||||
|
||||
# SHTTP servers - Streamable HTTP transport (recommended)
|
||||
#shttp_servers = [
|
||||
# # Basic SHTTP server with default 60s timeout
|
||||
# "https://api.example.com/mcp/shttp",
|
||||
#
|
||||
# # SHTTP server with custom timeout for long-running tools
|
||||
# {
|
||||
# url = "https://api.example.com/mcp/shttp",
|
||||
# api_key = "your-api-key",
|
||||
# timeout = 180 # 3 minutes for processing-heavy tools (1-3600 seconds)
|
||||
# }
|
||||
#]
|
||||
|
||||
# Stdio servers - Direct process communication (development only)
|
||||
#stdio_servers = [
|
||||
# # Basic stdio server
|
||||
# {name = "filesystem", command = "npx", args = ["@modelcontextprotocol/server-filesystem", "/"]},
|
||||
#
|
||||
# # Stdio server with environment variables
|
||||
# {
|
||||
# name = "fetch",
|
||||
# command = "uvx",
|
||||
# args = ["mcp-server-fetch"],
|
||||
# env = {DEBUG = "true"}
|
||||
# }
|
||||
#]
|
||||
|
||||
#################################### Model Routing ############################
|
||||
# Configuration for experimental model routing feature
|
||||
# Enables intelligent switching between different LLM models for specific purposes
|
||||
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 663 KiB |
@@ -8,21 +8,9 @@ description: This guide walks you through the process of installing OpenHands Cl
|
||||
|
||||
- Signed in to [OpenHands Cloud](https://app.all-hands.dev) with [a Bitbucket account](/usage/cloud/openhands-cloud).
|
||||
|
||||
## Adding Bitbucket Repository Access
|
||||
|
||||
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
|
||||
|
||||
## Working With Bitbucket Repos in Openhands Cloud
|
||||
|
||||
After signing in with a Bitbucket account, use the `Open Repository` section to select the appropriate repository and
|
||||
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
|
||||

|
||||
|
||||
## IP Whitelisting
|
||||
|
||||
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow
|
||||
OpenHands to access your repositories:
|
||||
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow OpenHands to access your repositories:
|
||||
|
||||
### Core App IP
|
||||
```
|
||||
@@ -43,6 +31,17 @@ OpenHands to access your repositories:
|
||||
34.60.55.59
|
||||
```
|
||||
|
||||
## Adding Bitbucket Repository Access
|
||||
|
||||
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
|
||||
|
||||
## Working With Bitbucket Repos in Openhands Cloud
|
||||
|
||||
After signing in with a Bitbucket account, use the `select a repo` and `select a branch` dropdowns to select the
|
||||
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
|
||||

|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
|
||||
|
||||
@@ -12,10 +12,13 @@ For the available API endpoints, refer to the
|
||||
To use the OpenHands Cloud API, you'll need to generate an API key:
|
||||
|
||||
1. Log in to your [OpenHands Cloud](https://app.all-hands.dev) account.
|
||||
2. Navigate to the [Settings > API Keys](https://app.all-hands.dev/settings/api-keys) page.
|
||||
3. Click `Create API Key`.
|
||||
4. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
|
||||
5. Copy the generated API key and store it securely. It will only be shown once.
|
||||
2. Navigate to the [Settings page](https://app.all-hands.dev/settings).
|
||||
3. Select the `API Keys` tab.
|
||||
4. Click `Create API Key`.
|
||||
5. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
|
||||
6. Copy the generated API key and store it securely. It will only be shown once.
|
||||
|
||||

|
||||
|
||||
## API Usage
|
||||
|
||||
|
||||
@@ -8,39 +8,24 @@ description: The Cloud UI provides a web interface for interacting with OpenHand
|
||||
|
||||
The landing page is where you can:
|
||||
|
||||
- [Add GitHub repository access](/usage/cloud/github-installation#adding-github-repository-access) to OpenHands.
|
||||
- [Select a GitHub repo](/usage/cloud/github-installation#working-with-github-repos-in-openhands-cloud),
|
||||
[a GitLab repo](/usage/cloud/gitlab-installation#working-with-gitlab-repos-in-openhands-cloud) or
|
||||
[a Bitbucket repo](/usage/cloud/bitbucket-installation#working-with-bitbucket-repos-in-openhands-cloud) to start working on.
|
||||
- Launch an empty conversation using `New Conversation`.
|
||||
- See `Suggested Tasks` for repositories that OpenHands has access to.
|
||||
- See your `Recent Conversations`.
|
||||
- Launch an empty conversation using `Launch from Scratch`.
|
||||
|
||||
## Settings
|
||||
|
||||
Settings are divided across tabs, with each tab focusing on a specific area of configuration.
|
||||
The Settings page allows you to:
|
||||
|
||||
- `User`
|
||||
- Change your email address.
|
||||
- `Integrations`
|
||||
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
|
||||
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
|
||||
- `Application`
|
||||
- Set your preferred language, notifications and other preferences.
|
||||
- Toggle task suggestions on GitHub.
|
||||
- Toggle Solvability Analysis.
|
||||
- Set a maximum budget per conversation.
|
||||
- Configure the username and email that OpenHands uses for commits.
|
||||
- `LLM` (Available for `Pro Users`)
|
||||
- Choose to use another LLM or use different models from the OpenHands provider.
|
||||
- `Billing`
|
||||
- Add credits for using the OpenHands provider.
|
||||
- Cancel your `Pro` subscription.
|
||||
- `Secrets`
|
||||
- [Generate custom secrets](/usage/common-settings#secrets-management).
|
||||
- `API Keys`
|
||||
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
|
||||
- `MCP`
|
||||
- [Setup an MCP server](/usage/mcp)
|
||||
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
|
||||
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
|
||||
- Set application settings like your preferred language, notifications and other preferences.
|
||||
- Add credits to your account.
|
||||
- [Generate custom secrets](/usage/common-settings#secrets-management).
|
||||
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
|
||||
- Change your email address.
|
||||
|
||||
## Key Features
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ description: This guide walks you through the process of installing OpenHands Cl
|
||||
|
||||
You can grant OpenHands access to specific GitHub repositories:
|
||||
|
||||
1. Click on `+ Add GitHub Repos` in the repository selection dropdown.
|
||||
1. Click on `Add GitHub repos` on the landing page.
|
||||
2. Select your organization and choose the specific repositories to grant OpenHands access to.
|
||||
<Accordion title="OpenHands permissions">
|
||||
- OpenHands requests short-lived tokens (8-hour expiration) with these permissions:
|
||||
@@ -34,22 +34,20 @@ You can grant OpenHands access to specific GitHub repositories:
|
||||
## Modifying Repository Access
|
||||
|
||||
You can modify GitHub repository access at any time by:
|
||||
- Selecting `+ Add GitHub Repos` in the repository selection dropdown or
|
||||
- Visiting the `Settings > Integrations` page and selecting `Configure GitHub Repositories`
|
||||
- Selecting `Add GitHub repos` on the landing page or
|
||||
- Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Integrations` tab
|
||||
|
||||
## Working With GitHub Repos in Openhands Cloud
|
||||
|
||||
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the
|
||||
`Open Repository` section to select the appropriate repository and branch you'd like OpenHands to work on. Then click
|
||||
on `Launch` to start the conversation!
|
||||
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the `select a repo`
|
||||
and `select a branch` dropdowns to select the appropriate repository and branch you'd like OpenHands to work on. Then
|
||||
click on `Launch` to start the conversation!
|
||||
|
||||

|
||||
|
||||
## Working on GitHub Issues and Pull Requests Using Openhands
|
||||
## Working on Github Issues and Pull Requests Using Openhands
|
||||
|
||||
To allow OpenHands to work directly from GitHub directly, you must
|
||||
[give OpenHands access to your repository](/usage/cloud/github-installation#modifying-repository-access). Once access is
|
||||
given, you can use OpenHands by labeling the issue or by tagging `@openhands`.
|
||||
Giving GitHub repository access to OpenHands also allows you to work on GitHub issues and pull requests directly.
|
||||
|
||||
### Working with Issues
|
||||
|
||||
@@ -66,12 +64,7 @@ To get OpenHands to work on pull requests, mention `@openhands` in the comments
|
||||
- Request updates
|
||||
- Get code explanations
|
||||
|
||||
<Note>
|
||||
The `@openhands` mention functionality in pull requests only works if the pull request is both
|
||||
*to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate
|
||||
permissions to access both repositories.
|
||||
</Note>
|
||||
|
||||
**Important Note**: The `@openhands` mention functionality in pull requests only works if the pull request is both *to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate permissions to access both repositories.
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -14,17 +14,16 @@ Upon signing into OpenHands Cloud with a GitLab account, OpenHands will have acc
|
||||
|
||||
## Working With GitLab Repos in Openhands Cloud
|
||||
|
||||
After signing in with a Gitlab account, use the `Open Repository` section to select the appropriate repository and
|
||||
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
After signing in with a Gitlab account, use the `select a repo` and `select a branch` dropdowns to select the
|
||||
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
|
||||

|
||||

|
||||
|
||||
## Using Tokens with Reduced Scopes
|
||||
|
||||
OpenHands requests an API-scoped token during OAuth authentication. By default, this token is provided to the agent.
|
||||
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default
|
||||
token assigned to the agent. While the high-permission API token is still requested and used for other components of
|
||||
the application (e.g. opening merge requests), the agent will not have access to it.
|
||||
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default token assigned to the agent.
|
||||
While the high-permission API token is still requested and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
|
||||
|
||||
## Working on GitLab Issues and Merge Requests Using Openhands
|
||||
|
||||
@@ -33,8 +32,7 @@ This feature works for personal projects and is available for group projects wit
|
||||
[Premium or Ultimate tier subscription](https://docs.gitlab.com/user/project/integrations/webhooks/#group-webhooks).
|
||||
|
||||
A webhook is automatically installed within a few minutes after the owner/maintainer of the project or group logs into
|
||||
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but
|
||||
we are planning to improve this in a future release.
|
||||
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but we are planning to improve this in a future release.
|
||||
</Note>
|
||||
|
||||
Giving GitLab repository access to OpenHands also allows you to work on GitLab issues and merge requests directly.
|
||||
|
||||
@@ -13,9 +13,7 @@ description: This guide walks you through installing the OpenHands Slack app.
|
||||
</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.
|
||||
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
|
||||
|
||||
@@ -67,19 +67,6 @@ sse_servers = [
|
||||
# External MCP service with authentication
|
||||
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
|
||||
]
|
||||
|
||||
# SHTTP Servers - Modern streamable HTTP transport (recommended)
|
||||
shttp_servers = [
|
||||
# Basic SHTTP server with default 60s timeout
|
||||
"https://api.example.com/mcp/shttp",
|
||||
|
||||
# Server with custom timeout for heavy operations
|
||||
{
|
||||
url = "https://files.example.com/mcp/shttp",
|
||||
api_key = "your-api-key",
|
||||
timeout = 1800 # 30 minutes for large file processing
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
@@ -131,17 +118,6 @@ SHTTP (Streamable HTTP) servers are configured using either a string URL or an o
|
||||
- Type: `str`
|
||||
- Description: API key for authentication
|
||||
|
||||
- `timeout` (optional)
|
||||
- Type: `int`
|
||||
- Default: `60`
|
||||
- Range: `1-3600` seconds (1 hour maximum)
|
||||
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
|
||||
- **Use Cases:**
|
||||
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries
|
||||
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls
|
||||
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations
|
||||
- **Note**: This timeout only applies to individual tool calls, not server connection establishment.
|
||||
|
||||
### Stdio Servers
|
||||
|
||||
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
|
||||
@@ -216,27 +192,5 @@ SHTTP is the modern HTTP-based transport protocol that provides enhanced feature
|
||||
|
||||
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
|
||||
|
||||
#### SHTTP Timeout Best Practices
|
||||
|
||||
When configuring SHTTP timeouts, consider these guidelines:
|
||||
|
||||
**Timeout Selection:**
|
||||
- **Database queries**: 30-60 seconds
|
||||
- **File operations**: 60-300 seconds (depending on file size)
|
||||
- **Web scraping**: 60-120 seconds
|
||||
- **Complex calculations**: 300-1800 seconds
|
||||
- **Batch processing**: 1800-3600 seconds (maximum)
|
||||
|
||||
**Error Handling:**
|
||||
When a tool call exceeds the configured timeout:
|
||||
- The operation is cancelled with an `asyncio.TimeoutError`
|
||||
- The agent receives a timeout error message
|
||||
- The server connection remains active for subsequent requests
|
||||
|
||||
**Monitoring:**
|
||||
- Set timeouts based on your tool's actual performance characteristics
|
||||
- Monitor timeout occurrences to optimize timeout values
|
||||
- Consider implementing server-side timeout handling for graceful degradation
|
||||
|
||||
### Standard Input/Output (stdio)
|
||||
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.
|
||||
|
||||
@@ -766,7 +766,7 @@ version = "1.17.1"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "test"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
@@ -836,7 +836,6 @@ files = [
|
||||
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
|
||||
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
@@ -1902,25 +1901,25 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.117.1"
|
||||
version = "0.116.1"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552"},
|
||||
{file = "fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a"},
|
||||
{file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"},
|
||||
{file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.40.0,<0.49.0"
|
||||
starlette = ">=0.40.0,<0.48.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastjsonschema"
|
||||
@@ -2292,72 +2291,6 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto
|
||||
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""]
|
||||
tqdm = ["tqdm"]
|
||||
|
||||
[[package]]
|
||||
name = "gevent"
|
||||
version = "25.9.1"
|
||||
description = "Coroutine-based network library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f18f80aef6b1f6907219affe15b36677904f7cfeed1f6a6bc198616e507ae2d7"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b274a53e818124a281540ebb4e7a2c524778f745b7a99b01bdecf0ca3ac0ddb0"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-win32.whl", hash = "sha256:c6c91f7e33c7f01237755884316110ee7ea076f5bdb9aa0982b6dc63243c0a38"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a44b0121f3d7c800740ff80351c897e85e76a7e4764690f35c5ad9ec17de5"},
|
||||
{file = "gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""}
|
||||
"zope.event" = "*"
|
||||
"zope.interface" = "*"
|
||||
|
||||
[package.extras]
|
||||
dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""]
|
||||
docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"]
|
||||
monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
|
||||
recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
|
||||
test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "gitdb"
|
||||
version = "4.0.12"
|
||||
@@ -2774,7 +2707,7 @@ version = "3.2.4"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "test"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"},
|
||||
@@ -2831,7 +2764,6 @@ files = [
|
||||
{file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"},
|
||||
{file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "furo"]
|
||||
@@ -5431,7 +5363,7 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
|
||||
|
||||
[[package]]
|
||||
name = "openhands-ai"
|
||||
version = "0.57.0"
|
||||
version = "0.55.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
optional = false
|
||||
python-versions = "^3.12,<3.14"
|
||||
@@ -5465,7 +5397,7 @@ json-repair = "*"
|
||||
jupyter_kernel_gateway = "*"
|
||||
kubernetes = "^33.1.0"
|
||||
libtmux = ">=0.37,<0.40"
|
||||
litellm = ">=1.74.3, <1.77.2, !=1.64.4, !=1.67.*"
|
||||
litellm = "^1.74.3, !=1.64.4, !=1.67.*"
|
||||
memory-profiler = "^0.61.0"
|
||||
numpy = "*"
|
||||
openai = "1.99.9"
|
||||
@@ -5474,7 +5406,6 @@ opentelemetry-api = "^1.33.1"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
|
||||
pathspec = "^0.12.1"
|
||||
pexpect = "*"
|
||||
pillow = "^11.3.0"
|
||||
poetry = "^2.1.2"
|
||||
prompt-toolkit = "^3.0.50"
|
||||
protobuf = "^5.0.0,<6.0.0"
|
||||
@@ -5482,7 +5413,6 @@ psutil = "*"
|
||||
pygithub = "^2.5.0"
|
||||
pyjwt = "^2.9.0"
|
||||
pylatexenc = "*"
|
||||
pypdf = "^6.0.0"
|
||||
PyPDF2 = "*"
|
||||
python-docx = "*"
|
||||
python-dotenv = "*"
|
||||
@@ -5496,17 +5426,13 @@ pyyaml = "^6.0.2"
|
||||
qtconsole = "^5.6.1"
|
||||
rapidfuzz = "^3.9.0"
|
||||
redis = ">=5.2,<7.0"
|
||||
requests = "^2.32.5"
|
||||
setuptools = ">=78.1.1"
|
||||
shellingham = "^1.5.4"
|
||||
sse-starlette = "^3.0.2"
|
||||
starlette = "^0.48.0"
|
||||
sse-starlette = "^2.1.3"
|
||||
tenacity = ">=8.5,<10.0"
|
||||
termcolor = "*"
|
||||
toml = "*"
|
||||
tornado = "*"
|
||||
types-toml = "*"
|
||||
urllib3 = "^2.5.0"
|
||||
uvicorn = "*"
|
||||
whatthepatch = "^1.0.6"
|
||||
zope-interface = "7.2"
|
||||
@@ -6545,12 +6471,11 @@ version = "2.22"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "test"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
@@ -8340,7 +8265,7 @@ version = "80.9.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "test"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
|
||||
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
|
||||
@@ -8706,14 +8631,14 @@ sqlcipher = ["sqlcipher3_binary"]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.0.2"
|
||||
version = "2.4.1"
|
||||
description = "SSE plugin for Starlette"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"},
|
||||
{file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"},
|
||||
{file = "sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a"},
|
||||
{file = "sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -8721,7 +8646,7 @@ anyio = ">=4.7.0"
|
||||
|
||||
[package.extras]
|
||||
daphne = ["daphne (>=4.2.0)"]
|
||||
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
|
||||
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio,examples] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
|
||||
granian = ["granian (>=2.3.1)"]
|
||||
uvicorn = ["uvicorn (>=0.34.0)"]
|
||||
|
||||
@@ -8777,14 +8702,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.48.0"
|
||||
version = "0.47.3"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659"},
|
||||
{file = "starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46"},
|
||||
{file = "starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51"},
|
||||
{file = "starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -9913,32 +9838,13 @@ enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "zope-event"
|
||||
version = "6.0"
|
||||
description = "Very basic event publishing system"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3"},
|
||||
{file = "zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
setuptools = ">=75.8.2"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx"]
|
||||
test = ["zope.testrunner (>=6.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "zope-interface"
|
||||
version = "7.2"
|
||||
description = "Interfaces for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "test"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"},
|
||||
@@ -10102,4 +10008,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12,<3.14"
|
||||
content-hash = "8c460070dce6bdec5ee0ee7bc0c2246fcf2602d1e64a0867b4f5e3a0e334fe93"
|
||||
content-hash = "5771671ef2acc36e7b0931c73fa035ca1d329e8dac6827f7a349e1a569c3fd23"
|
||||
|
||||
@@ -63,7 +63,6 @@ openai = "*"
|
||||
opencv-python = "*"
|
||||
pandas = "*"
|
||||
reportlab = "*"
|
||||
gevent = ">=24.2.1,<26.0.0"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
@@ -86,7 +85,3 @@ lint.pydocstyle.convention = "google"
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
|
||||
[tool.coverage.run]
|
||||
relative_files = true
|
||||
omit = [ "tests/*" ]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test
|
||||
@@ -13,8 +13,7 @@ vi.mock("react-router", async () => {
|
||||
|
||||
vi.mock("#/context/conversation-context", () => ({
|
||||
useConversation: () => ({ conversationId: "test-conversation-id" }),
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) =>
|
||||
children,
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", async () => {
|
||||
@@ -30,18 +29,21 @@ vi.mock("react-i18next", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Zustand browser store
|
||||
// Mock redux
|
||||
const mockDispatch = vi.fn();
|
||||
let mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("#/stores/browser-store", () => ({
|
||||
useBrowserStore: () => mockBrowserState,
|
||||
}));
|
||||
vi.mock("react-redux", async () => {
|
||||
const actual = await vi.importActual("react-redux");
|
||||
return {
|
||||
...actual,
|
||||
useDispatch: () => mockDispatch,
|
||||
useSelector: () => mockBrowserState,
|
||||
};
|
||||
});
|
||||
|
||||
// Import the component after all mocks are set up
|
||||
import { BrowserPanel } from "#/components/features/browser/browser";
|
||||
@@ -53,9 +55,6 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -64,9 +63,6 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
@@ -79,11 +75,7 @@ describe("Browser", () => {
|
||||
// Set the mock state for this test
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
|
||||
@@ -12,7 +12,6 @@ import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
// Mock hooks
|
||||
const mockUseUserProviders = vi.fn();
|
||||
@@ -56,47 +55,20 @@ describe("MicroagentManagement", () => {
|
||||
]);
|
||||
|
||||
const renderMicroagentManagement = (config?: QueryClientConfig) =>
|
||||
renderWithProviders(<RouterStub />);
|
||||
|
||||
// Common test data
|
||||
const testRepository = {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github" as const,
|
||||
is_public: true,
|
||||
owner_type: "user" as const,
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
};
|
||||
|
||||
// Helper function to render with custom Zustand store state
|
||||
const renderWithCustomStore = (storeOverrides: Partial<any>) => {
|
||||
useMicroagentManagementStore.setState(storeOverrides);
|
||||
return renderWithProviders(<RouterStub />);
|
||||
};
|
||||
|
||||
// Helper function to render with update modal visible
|
||||
const renderWithUpdateModal = (additionalState: Partial<any> = {}) => {
|
||||
return renderWithCustomStore({
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: testRepository,
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to render with selected microagent
|
||||
const renderWithSelectedMicroagent = (
|
||||
microagent: any,
|
||||
additionalState: Partial<any> = {},
|
||||
) => {
|
||||
return renderWithCustomStore({
|
||||
selectedRepository: testRepository,
|
||||
selectedMicroagentItem: {
|
||||
microagent,
|
||||
conversation: null,
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem: null,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
@@ -209,23 +181,6 @@ describe("MicroagentManagement", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Reset Zustand store to default state
|
||||
useMicroagentManagementStore.setState({
|
||||
// Modal visibility states
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
|
||||
// Repository states
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
|
||||
// Microagent states
|
||||
selectedMicroagentItem: null,
|
||||
});
|
||||
|
||||
// Setup default hook mocks
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: ["github"],
|
||||
@@ -1387,10 +1342,28 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render modal when Zustand state is set to visible", async () => {
|
||||
// Render with modal already visible in Zustand state
|
||||
renderWithCustomStore({
|
||||
addMicroagentModalVisible: true,
|
||||
it("should render modal when Redux state is set to visible", async () => {
|
||||
// Render with modal already visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: true, // Start with modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that modal is rendered
|
||||
@@ -1660,16 +1633,29 @@ describe("MicroagentManagement", () => {
|
||||
pr_number: null,
|
||||
};
|
||||
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) => {
|
||||
// Set the store with the selected microagent item and a repository
|
||||
useMicroagentManagementStore.setState({
|
||||
selectedMicroagentItem,
|
||||
selectedRepository: testRepository,
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) =>
|
||||
renderWithProviders(<MicroagentManagementMain />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return renderWithProviders(<MicroagentManagementMain />);
|
||||
};
|
||||
|
||||
it("should render MicroagentManagementDefault when no microagent or conversation is selected", async () => {
|
||||
renderMicroagentManagementMain(null);
|
||||
|
||||
@@ -1994,8 +1980,31 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
it("should render update microagent modal when updateMicroagentModalVisible is true", async () => {
|
||||
// Render with update modal visible in Zustand state
|
||||
renderWithUpdateModal();
|
||||
// Render with update modal visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true, // Start with update modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that update modal is rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2006,7 +2015,30 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should display update microagent title when isUpdate is true", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the update title is displayed
|
||||
expect(
|
||||
@@ -2016,10 +2048,28 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should populate form fields with existing microagent data when updating", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithUpdateModal({
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: null,
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2036,7 +2086,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2064,7 +2137,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2087,7 +2183,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2113,7 +2232,27 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle update modal with empty microagent data", async () => {
|
||||
// Render with update modal visible but no microagent data
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that update modal is still rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2134,7 +2273,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for the content to be loaded and check that the form field is empty
|
||||
await waitFor(() => {
|
||||
@@ -2155,7 +2317,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the modal is rendered correctly
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2314,7 +2499,30 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should render learn something new button in microagent view", async () => {
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the learn something new button is displayed
|
||||
expect(
|
||||
@@ -2326,7 +2534,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2355,7 +2586,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2387,7 +2641,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2417,7 +2694,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
|
||||
@@ -342,7 +342,13 @@ describe("InteractiveChatBox", () => {
|
||||
// Simulate parent component updating the value prop
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox onSubmit={onSubmit} onStop={onStop} />
|
||||
<InteractiveChatBox
|
||||
onSubmit={onSubmit}
|
||||
onStop={onStop}
|
||||
isWaitingForUserInput={true}
|
||||
hasSubstantiveAgentActions={true}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { JupyterEditor } from "#/components/features/jupyter/jupyter";
|
||||
import { useJupyterStore } from "#/state/jupyter-store";
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { jupyterReducer } from "#/state/jupyter-slice";
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
|
||||
describe("JupyterEditor", () => {
|
||||
const mockStore = configureStore({
|
||||
@@ -15,20 +15,19 @@ describe("JupyterEditor", () => {
|
||||
code: () => ({}),
|
||||
cmd: () => ({}),
|
||||
agent: () => ({}),
|
||||
jupyter: jupyterReducer,
|
||||
securityAnalyzer: () => ({}),
|
||||
status: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the Zustand store before each test
|
||||
useJupyterStore.setState({
|
||||
cells: Array(20).fill({
|
||||
content: "Test cell content",
|
||||
type: "input",
|
||||
imageUrls: undefined,
|
||||
}),
|
||||
});
|
||||
preloadedState: {
|
||||
jupyter: {
|
||||
cells: Array(20).fill({
|
||||
content: "Test cell content",
|
||||
type: "input",
|
||||
output: "Test output",
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("should have a scrollable container", () => {
|
||||
@@ -37,7 +36,7 @@ describe("JupyterEditor", () => {
|
||||
<div style={{ height: "100vh" }}>
|
||||
<JupyterEditor maxWidth={800} />
|
||||
</div>
|
||||
</Provider>,
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const container = screen.getByTestId("jupyter-container");
|
||||
|
||||
@@ -21,12 +21,8 @@ vi.mock("#/state/command-store", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/state/jupyter-store", () => ({
|
||||
useJupyterStore: {
|
||||
getState: () => ({
|
||||
appendJupyterInput: mockAppendJupyterInput,
|
||||
}),
|
||||
},
|
||||
vi.mock("#/state/jupyter-slice", () => ({
|
||||
appendJupyterInput: mockAppendJupyterInput,
|
||||
}));
|
||||
|
||||
vi.mock("#/state/metrics-slice", () => ({
|
||||
@@ -85,8 +81,8 @@ describe("handleActionMessage", () => {
|
||||
handleActionMessage(ipythonAction);
|
||||
|
||||
// Check that appendJupyterInput was called with the code
|
||||
expect(mockAppendJupyterInput).toHaveBeenCalledWith(
|
||||
"print('Hello from Jupyter!')",
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
mockAppendJupyterInput("print('Hello from Jupyter!')"),
|
||||
);
|
||||
expect(mockAppendInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -60,7 +60,13 @@ describe("Check for hardcoded English strings", () => {
|
||||
test("InteractiveChatBox should not have hardcoded English strings", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox onSubmit={() => {}} onStop={() => {}} />
|
||||
<InteractiveChatBox
|
||||
onSubmit={() => {}}
|
||||
onStop={() => {}}
|
||||
isWaitingForUserInput={false}
|
||||
hasSubstantiveAgentActions={false}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -7,38 +7,38 @@
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.9.1",
|
||||
"@react-router/serve": "^7.9.1",
|
||||
"@react-router/node": "^7.8.2",
|
||||
"@react-router/serve": "^7.8.2",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@stripe/react-stripe-js": "^4.0.2",
|
||||
"@stripe/react-stripe-js": "^4.0.0",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tanstack/react-query": "^5.87.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "^9.0.10",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.19",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.30",
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"posthog-js": "^1.268.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -47,7 +47,8 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.9.1",
|
||||
"react-resizable-panels": "^3.0.5",
|
||||
"react-router": "^7.8.2",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
@@ -55,7 +56,7 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.1.4",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2",
|
||||
"zustand": "^5.0.8"
|
||||
@@ -97,16 +98,16 @@
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@react-router/dev": "^7.9.1",
|
||||
"@tailwindcss/typography": "^0.5.18",
|
||||
"@tanstack/eslint-plugin-query": "^5.90.1",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@react-router/dev": "^7.8.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.86.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -128,8 +129,8 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.0.0",
|
||||
"lint-staged": "^16.2.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.6",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.5.0",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.11.1'
|
||||
const PACKAGE_VERSION = '2.10.5'
|
||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { BrowserSnapshot } from "./browser-snapshot";
|
||||
import { EmptyBrowserMessage } from "./empty-browser-message";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
setUrl,
|
||||
setScreenshotSrc,
|
||||
} from "#/state/browser-slice";
|
||||
|
||||
export function BrowserPanel() {
|
||||
const { url, screenshotSrc, reset } = useBrowserStore();
|
||||
const { url, screenshotSrc } = useSelector(
|
||||
(state: RootState) => state.browser,
|
||||
);
|
||||
const { conversationId } = useConversationId();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
}, [conversationId, reset]);
|
||||
dispatch(setUrl(browserInitialState.url));
|
||||
dispatch(setScreenshotSrc(browserInitialState.screenshotSrc));
|
||||
}, [conversationId]);
|
||||
|
||||
const imgSrc =
|
||||
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
|
||||
|
||||
function getEntryPoint(
|
||||
@@ -46,7 +46,7 @@ function getEntryPoint(
|
||||
}
|
||||
|
||||
export function ChatInterface() {
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const { getErrorMessage } = useWSErrorMessage();
|
||||
const { send, isLoadingMessages, parsedEvents } = useWsClient();
|
||||
const { setOptimisticUserMessage, getOptimisticUserMessage } =
|
||||
@@ -141,7 +141,7 @@ export function ChatInterface() {
|
||||
|
||||
send(createChatMessage(prompt, imageUrls, uploadedFiles, timestamp));
|
||||
setOptimisticUserMessage(content);
|
||||
setMessageToSend("");
|
||||
dispatch(setMessageToSend(null));
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
@@ -156,6 +156,10 @@ export function ChatInterface() {
|
||||
setFeedbackPolarity(polarity);
|
||||
};
|
||||
|
||||
const isWaitingForUserInput =
|
||||
curAgentState === AgentState.AWAITING_USER_INPUT ||
|
||||
curAgentState === AgentState.FINISHED;
|
||||
|
||||
// Create a ScrollProvider with the scroll hook values
|
||||
const scrollProviderValue = {
|
||||
scrollRef,
|
||||
@@ -176,7 +180,9 @@ export function ChatInterface() {
|
||||
!optimisticUserMessage &&
|
||||
!userEventsExist && (
|
||||
<ChatSuggestions
|
||||
onSuggestionsClick={(message) => setMessageToSend(message)}
|
||||
onSuggestionsClick={(message) =>
|
||||
dispatch(setMessageToSend(message))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Note: We only hide chat suggestions when there's a user message */}
|
||||
@@ -231,6 +237,9 @@ export function ChatInterface() {
|
||||
<InteractiveChatBox
|
||||
onSubmit={handleSendMessage}
|
||||
onStop={handleStop}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={!!optimisticUserMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export function ChatMessage({
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className={cn(
|
||||
"rounded-xl relative w-fit max-w-full last:mb-4",
|
||||
"rounded-xl relative w-fit max-w-full",
|
||||
"flex flex-col gap-2",
|
||||
type === "user" && " p-4 bg-tertiary self-end",
|
||||
type === "agent" && "mt-6 max-w-full bg-transparent",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Suggestions } from "#/components/features/suggestions/suggestions";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface ChatSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -12,7 +13,9 @@ interface ChatSuggestionsProps {
|
||||
|
||||
export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { shouldHideSuggestions } = useConversationStore();
|
||||
const shouldHideSuggestions = useSelector(
|
||||
(state: RootState) => state.conversation.shouldHideSuggestions,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import {
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { useChatInputLogic } from "#/hooks/chat/use-chat-input-logic";
|
||||
import { useFileHandling } from "#/hooks/chat/use-file-handling";
|
||||
import { useGripResize } from "#/hooks/chat/use-grip-resize";
|
||||
@@ -8,7 +15,6 @@ import { useChatSubmission } from "#/hooks/chat/use-chat-submission";
|
||||
import { ChatInputGrip } from "./components/chat-input-grip";
|
||||
import { ChatInputContainer } from "./components/chat-input-container";
|
||||
import { HiddenFileInput } from "./components/hidden-file-input";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export interface CustomChatInputProps {
|
||||
disabled?: boolean;
|
||||
@@ -35,12 +41,10 @@ export function CustomChatInput({
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
}: CustomChatInputProps) {
|
||||
const {
|
||||
submittedMessage,
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
} = useConversationStore();
|
||||
const { submittedMessage } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Disable input when conversation is stopped
|
||||
const isConversationStopped = conversationStatus === "STOPPED";
|
||||
@@ -52,8 +56,8 @@ export function CustomChatInput({
|
||||
return;
|
||||
}
|
||||
onSubmit(submittedMessage);
|
||||
setSubmittedMessage(null);
|
||||
}, [submittedMessage, disabled, onSubmit, setSubmittedMessage]);
|
||||
dispatch(setSubmittedMessage(null));
|
||||
}, [submittedMessage, disabled, onSubmit, dispatch]);
|
||||
|
||||
// Custom hooks
|
||||
const {
|
||||
@@ -108,10 +112,10 @@ export function CustomChatInput({
|
||||
// Cleanup: reset suggestions visibility when component unmounts
|
||||
useEffect(
|
||||
() => () => {
|
||||
setShouldHideSuggestions(false);
|
||||
clearAllFiles();
|
||||
dispatch(setShouldHideSuggestions(false));
|
||||
dispatch(clearAllFiles());
|
||||
},
|
||||
[setShouldHideSuggestions, clearAllFiles],
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import {
|
||||
FileWriteAction,
|
||||
CommandAction,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { ErrorMessage } from "../error-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface ErrorEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ErrorEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: ErrorEventMessageProps) {
|
||||
if (!isErrorObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isFinishAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface FinishEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function FinishEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: FinishEventMessageProps) {
|
||||
if (!isFinishAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface GenericEventMessageWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function GenericEventMessageWrapper({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: GenericEventMessageWrapperProps) {
|
||||
return (
|
||||
<div>
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export { ErrorEventMessage } from "./error-event-message";
|
||||
export { UserAssistantEventMessage } from "./user-assistant-event-message";
|
||||
export { FinishEventMessage } from "./finish-event-message";
|
||||
export { RejectEventMessage } from "./reject-event-message";
|
||||
export { McpEventMessage } from "./mcp-event-message";
|
||||
export { TaskTrackingEventMessage } from "./task-tracking-event-message";
|
||||
export { ObservationPairEventMessage } from "./observation-pair-event-message";
|
||||
export { GenericEventMessageWrapper } from "./generic-event-message-wrapper";
|
||||
export { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
export { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { LikertScale } from "../../feedback/likert-scale";
|
||||
|
||||
interface LikertScaleWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function LikertScaleWrapper({
|
||||
event,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: LikertScaleWrapperProps) {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isMcpObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { MCPObservationContent } from "../mcp-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface McpEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function McpEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: McpEventMessageProps) {
|
||||
if (!isMcpObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "../microagent/microagent-status-indicator";
|
||||
|
||||
interface MicroagentStatusWrapperProps {
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function MicroagentStatusWrapper({
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: MicroagentStatusWrapperProps) {
|
||||
if (!microagentStatus || !actions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface ObservationPairEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function ObservationPairEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: ObservationPairEventMessageProps) {
|
||||
if (!isOpenHandsAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isRejectObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
|
||||
interface RejectEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
}
|
||||
|
||||
export function RejectEventMessage({ event }: RejectEventMessageProps) {
|
||||
if (!isRejectObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isTaskTrackingObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { TaskTrackingObservationContent } from "../task-tracking-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface TaskTrackingEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function TaskTrackingEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: TaskTrackingEventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isTaskTrackingObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { command } = event.extras;
|
||||
let title: React.ReactNode;
|
||||
let initiallyExpanded = false;
|
||||
|
||||
// Determine title and expansion state based on command
|
||||
if (command === "plan") {
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
|
||||
initiallyExpanded = true;
|
||||
} else {
|
||||
// command === "view"
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
|
||||
initiallyExpanded = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={<TaskTrackingObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
initiallyExpanded={initiallyExpanded}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isUserMessage, isAssistantMessage } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { ImageCarousel } from "../../images/image-carousel";
|
||||
import { FileList } from "../../files/file-list";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { parseMessageFromEvent } from "../event-content-helpers/parse-message-from-event";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface UserAssistantEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAssistantEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: UserAssistantEventMessageProps) {
|
||||
if (!isUserMessage(event) && !isAssistantMessage(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
{isAssistantMessage(event) && event.action === "message" && (
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,39 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import {
|
||||
isUserMessage,
|
||||
isErrorObservation,
|
||||
isAssistantMessage,
|
||||
isOpenHandsAction,
|
||||
isOpenHandsObservation,
|
||||
isFinishAction,
|
||||
isRejectObservation,
|
||||
isMcpObservation,
|
||||
isTaskTrackingObservation,
|
||||
} from "#/types/core/guards";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { ImageCarousel } from "../images/image-carousel";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { ErrorMessage } from "./error-message";
|
||||
import { MCPObservationContent } from "./mcp-observation-content";
|
||||
import { TaskTrackingObservationContent } from "./task-tracking-observation-content";
|
||||
import { getObservationResult } from "./event-content-helpers/get-observation-result";
|
||||
import { getEventContent } from "./event-content-helpers/get-event-content";
|
||||
import { GenericEventMessage } from "./generic-event-message";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "./microagent/microagent-status-indicator";
|
||||
import { FileList } from "../files/file-list";
|
||||
import { parseMessageFromEvent } from "./event-content-helpers/parse-message-from-event";
|
||||
import { LikertScale } from "../feedback/likert-scale";
|
||||
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useFeedbackExists } from "#/hooks/query/use-feedback-exists";
|
||||
import {
|
||||
ErrorEventMessage,
|
||||
UserAssistantEventMessage,
|
||||
FinishEventMessage,
|
||||
RejectEventMessage,
|
||||
McpEventMessage,
|
||||
TaskTrackingEventMessage,
|
||||
ObservationPairEventMessage,
|
||||
GenericEventMessageWrapper,
|
||||
} from "./event-message-components";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface EventMessageProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
@@ -41,7 +51,6 @@ interface EventMessageProps {
|
||||
isInLast10Actions: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
export function EventMessage({
|
||||
event,
|
||||
hasObservationPair,
|
||||
@@ -53,6 +62,7 @@ export function EventMessage({
|
||||
actions,
|
||||
isInLast10Actions,
|
||||
}: EventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShowConfirmationButtons =
|
||||
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
|
||||
|
||||
@@ -63,83 +73,194 @@ export function EventMessage({
|
||||
isLoading: isCheckingFeedback,
|
||||
} = useFeedbackExists(event.id);
|
||||
|
||||
// Common props for components that need them
|
||||
const commonProps = {
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
const renderLikertScale = () => {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Error observations
|
||||
if (isErrorObservation(event)) {
|
||||
return <ErrorEventMessage event={event} {...commonProps} />;
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Observation pairs with OpenHands actions
|
||||
if (hasObservationPair && isOpenHandsAction(event)) {
|
||||
return (
|
||||
<ObservationPairEventMessage
|
||||
event={event}
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return microagentStatus && actions ? (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
// Finish actions
|
||||
if (isFinishAction(event)) {
|
||||
return <FinishEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// User and assistant messages
|
||||
if (isUserMessage(event) || isAssistantMessage(event)) {
|
||||
return (
|
||||
<UserAssistantEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
{...commonProps}
|
||||
/>
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Reject observations
|
||||
if (isRejectObservation(event)) {
|
||||
return <RejectEventMessage event={event} />;
|
||||
if (isUserMessage(event) || isAssistantMessage(event)) {
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{isAssistantMessage(event) &&
|
||||
event.action === "message" &&
|
||||
renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isRejectObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// MCP observations
|
||||
if (isMcpObservation(event)) {
|
||||
return (
|
||||
<McpEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Task tracking observations
|
||||
if (isTaskTrackingObservation(event)) {
|
||||
const { command } = event.extras;
|
||||
let title: React.ReactNode;
|
||||
let initiallyExpanded = false;
|
||||
|
||||
// Determine title and expansion state based on command
|
||||
if (command === "plan") {
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
|
||||
initiallyExpanded = true;
|
||||
} else {
|
||||
// command === "view"
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
|
||||
initiallyExpanded = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<TaskTrackingEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={<TaskTrackingObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
initiallyExpanded={initiallyExpanded}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
return (
|
||||
<GenericEventMessageWrapper
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
<div>
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPrButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPrButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPrButtonProps) {
|
||||
@@ -22,7 +24,7 @@ export function GitControlBarPrButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
|
||||
const handlePrClick = () => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
|
||||
@@ -8,10 +8,12 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface GitControlBarPullButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBarPullButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
}: GitControlBarPullButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -20,7 +22,7 @@ export function GitControlBarPullButton({
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const hasRepository = conversation?.selected_repository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
|
||||
const handlePullClick = () => {
|
||||
posthog.capture("pull_button_clicked");
|
||||
|
||||
@@ -8,12 +8,14 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPushButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPushButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPushButtonProps) {
|
||||
@@ -22,7 +24,7 @@ export function GitControlBarPushButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
|
||||
const handlePushClick = () => {
|
||||
posthog.capture("push_button_clicked");
|
||||
|
||||
@@ -11,9 +11,17 @@ import { GitControlBarTooltipWrapper } from "./git-control-bar-tooltip-wrapper";
|
||||
|
||||
interface GitControlBarProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
export function GitControlBar({
|
||||
onSuggestionsClick,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: GitControlBarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: conversation } = useActiveConversation();
|
||||
@@ -22,6 +30,12 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
const gitProvider = conversation?.git_provider as Provider;
|
||||
const selectedBranch = conversation?.selected_branch;
|
||||
|
||||
// Button is enabled when the agent is waiting for user input, has substantive actions, and no optimistic message
|
||||
const isButtonEnabled =
|
||||
isWaitingForUserInput &&
|
||||
hasSubstantiveAgentActions &&
|
||||
!optimisticUserMessage;
|
||||
|
||||
const hasRepository = !!selectedRepository;
|
||||
|
||||
return (
|
||||
@@ -59,6 +73,7 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
>
|
||||
<GitControlBarPullButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
/>
|
||||
</GitControlBarTooltipWrapper>
|
||||
|
||||
@@ -69,6 +84,7 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
>
|
||||
<GitControlBarPushButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
@@ -81,6 +97,7 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
>
|
||||
<GitControlBarPrButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { isFileImage } from "#/utils/is-file-image";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { CustomChatInput } from "./custom-chat-input";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { GitControlBar } from "./git-control-bar";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import {
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} from "#/state/conversation-slice";
|
||||
import { processFiles, processImages } from "#/utils/file-processing";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface InteractiveChatBoxProps {
|
||||
onSubmit: (message: string, images: File[], files: File[]) => void;
|
||||
onStop: () => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function InteractiveChatBox({
|
||||
onSubmit,
|
||||
onStop,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: InteractiveChatBoxProps) {
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const curAgentState = useSelector(
|
||||
(state: RootState) => state.agent.curAgentState,
|
||||
);
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
// Helper function to validate and filter files
|
||||
@@ -52,24 +58,26 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to show loading indicators for files
|
||||
const showLoadingIndicators = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => addFileLoading(file.name));
|
||||
validImages.forEach((image) => addImageLoading(image.name));
|
||||
validFiles.forEach((file) => dispatch(addFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(addImageLoading(image.name)));
|
||||
};
|
||||
|
||||
// Helper function to handle successful file processing results
|
||||
const handleSuccessfulFiles = (fileResults: { successful: File[] }) => {
|
||||
if (fileResults.successful.length > 0) {
|
||||
addFiles(fileResults.successful);
|
||||
fileResults.successful.forEach((file) => removeFileLoading(file.name));
|
||||
dispatch(addFiles(fileResults.successful));
|
||||
fileResults.successful.forEach((file) =>
|
||||
dispatch(removeFileLoading(file.name)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to handle successful image processing results
|
||||
const handleSuccessfulImages = (imageResults: { successful: File[] }) => {
|
||||
if (imageResults.successful.length > 0) {
|
||||
addImages(imageResults.successful);
|
||||
dispatch(addImages(imageResults.successful));
|
||||
imageResults.successful.forEach((image) =>
|
||||
removeImageLoading(image.name),
|
||||
dispatch(removeImageLoading(image.name)),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -80,14 +88,14 @@ export function InteractiveChatBox({
|
||||
imageResults: { failed: { file: File; error: Error }[] },
|
||||
) => {
|
||||
fileResults.failed.forEach(({ file, error }) => {
|
||||
removeFileLoading(file.name);
|
||||
dispatch(removeFileLoading(file.name));
|
||||
displayErrorToast(
|
||||
`Failed to process file ${file.name}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
imageResults.failed.forEach(({ file, error }) => {
|
||||
removeImageLoading(file.name);
|
||||
dispatch(removeImageLoading(file.name));
|
||||
displayErrorToast(
|
||||
`Failed to process image ${file.name}: ${error.message}`,
|
||||
);
|
||||
@@ -96,8 +104,8 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to clear loading states on error
|
||||
const clearLoadingStates = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => removeFileLoading(file.name));
|
||||
validImages.forEach((image) => removeImageLoading(image.name));
|
||||
validFiles.forEach((file) => dispatch(removeFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(removeImageLoading(image.name)));
|
||||
};
|
||||
|
||||
const handleUpload = async (selectedFiles: File[]) => {
|
||||
@@ -132,7 +140,7 @@ export function InteractiveChatBox({
|
||||
|
||||
const handleSubmit = (message: string) => {
|
||||
onSubmit(message, images, files);
|
||||
clearAllFiles();
|
||||
dispatch(clearAllFiles());
|
||||
};
|
||||
|
||||
const handleSuggestionsClick = (suggestion: string) => {
|
||||
@@ -153,7 +161,12 @@ export function InteractiveChatBox({
|
||||
conversationStatus={conversation?.status || null}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<GitControlBar onSuggestionsClick={handleSuggestionsClick} />
|
||||
<GitControlBar
|
||||
onSuggestionsClick={handleSuggestionsClick}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={optimisticUserMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useHandleRuntimeActive } from "#/hooks/use-handle-runtime-active";
|
||||
import { LoadingMicroagentBody } from "./loading-microagent-body";
|
||||
import { LoadingMicroagentTextarea } from "./loading-microagent-textarea";
|
||||
import { useGetMicroagents } from "#/hooks/query/use-get-microagents";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface LaunchMicroagentModalProps {
|
||||
onClose: () => void;
|
||||
@@ -77,9 +76,9 @@ export function LaunchMicroagentModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Typography.Text className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
<span className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
{t("MICROAGENT$DEFINITION")}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
|
||||
<form
|
||||
data-testid="launch-microagent-modal"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
export function LoadingMicroagentBody() {
|
||||
const { t } = useTranslation();
|
||||
@@ -11,7 +10,7 @@ export function LoadingMicroagentBody() {
|
||||
{t("MICROAGENT$ADD_TO_MICROAGENT")}
|
||||
</h2>
|
||||
<Spinner size="lg" />
|
||||
<Typography.Text>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</Typography.Text>
|
||||
<p>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</p>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { SuccessIndicator } from "../success-indicator";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface MicroagentStatusIndicatorProps {
|
||||
status: MicroagentStatus;
|
||||
@@ -82,9 +81,7 @@ export function MicroagentStatusIndicator({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography.Text className="underline">{statusText}</Typography.Text>
|
||||
);
|
||||
return <span className="underline">{statusText}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { UploadedFile } from "./uploaded-file";
|
||||
import { UploadedImage } from "./uploaded-image";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { removeFile, removeImage } from "#/state/conversation-slice";
|
||||
|
||||
export function UploadedFiles() {
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
loadingFiles,
|
||||
loadingImages,
|
||||
removeFile,
|
||||
removeImage,
|
||||
} = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const loadingFiles = useSelector(
|
||||
(state: RootState) => state.conversation.loadingFiles,
|
||||
);
|
||||
const loadingImages = useSelector(
|
||||
(state: RootState) => state.conversation.loadingImages,
|
||||
);
|
||||
|
||||
const handleRemoveFile = (index: number) => {
|
||||
removeFile(index);
|
||||
dispatch(removeFile(index));
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
removeImage(index);
|
||||
dispatch(removeImage(index));
|
||||
};
|
||||
|
||||
// Don't render anything if there are no files, images, or loading items
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { RootState } from "#/store";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
@@ -12,7 +12,7 @@ import ClockIcon from "#/icons/u-clock-three.svg?react";
|
||||
import { ChatResumeAgentButton } from "../chat/chat-play-button";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { AgentLoading } from "./agent-loading";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setShouldShownAgentLoading } from "#/state/conversation-slice";
|
||||
import CircleErrorIcon from "#/icons/circle-error.svg?react";
|
||||
|
||||
export interface AgentStatusProps {
|
||||
@@ -29,7 +29,7 @@ export function AgentStatus({
|
||||
disabled = false,
|
||||
}: AgentStatusProps) {
|
||||
const { t } = useTranslation();
|
||||
const { setShouldShownAgentLoading } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curStatusMessage } = useStatusStore();
|
||||
const { webSocketStatus } = useWsClient();
|
||||
@@ -58,8 +58,8 @@ export function AgentStatus({
|
||||
|
||||
// Update global state when agent loading condition changes
|
||||
useEffect(() => {
|
||||
setShouldShownAgentLoading(shouldShownAgentLoading);
|
||||
}, [shouldShownAgentLoading, setShouldShownAgentLoading]);
|
||||
dispatch(setShouldShownAgentLoading(shouldShownAgentLoading));
|
||||
}, [shouldShownAgentLoading, dispatch]);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
getCreatePRPrompt,
|
||||
getCreateNewBranchPrompt,
|
||||
} from "#/utils/utils";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
|
||||
import ArrowUpIcon from "#/icons/u-arrow-up.svg?react";
|
||||
import ArrowDownIcon from "#/icons/u-arrow-down.svg?react";
|
||||
@@ -27,28 +28,28 @@ interface GitToolsSubmenuProps {
|
||||
|
||||
export function GitToolsSubmenu({ onClose }: GitToolsSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
const currentGitProvider = conversation?.git_provider as Provider;
|
||||
|
||||
const onGitPull = () => {
|
||||
setMessageToSend(getGitPullPrompt());
|
||||
dispatch(setMessageToSend(getGitPullPrompt()));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onGitPush = () => {
|
||||
setMessageToSend(getGitPushPrompt(currentGitProvider));
|
||||
dispatch(setMessageToSend(getGitPushPrompt(currentGitProvider)));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreatePR = () => {
|
||||
setMessageToSend(getCreatePRPrompt(currentGitProvider));
|
||||
dispatch(setMessageToSend(getCreatePRPrompt(currentGitProvider)));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreateNewBranch = () => {
|
||||
setMessageToSend(getCreateNewBranchPrompt());
|
||||
dispatch(setMessageToSend(getCreateNewBranchPrompt()));
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -8,7 +9,7 @@ import PrStatusIcon from "#/icons/pr-status.svg?react";
|
||||
import DocumentIcon from "#/icons/document.svg?react";
|
||||
import WaterIcon from "#/icons/u-water.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { REPO_SUGGESTIONS } from "#/utils/suggestions/repo-suggestions";
|
||||
import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants";
|
||||
|
||||
@@ -21,22 +22,22 @@ interface MacrosSubmenuProps {
|
||||
|
||||
export function MacrosSubmenu({ onClose }: MacrosSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onIncreaseTestCoverage = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE));
|
||||
onClose();
|
||||
};
|
||||
const onFixReadme = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.FIX_README);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.FIX_README));
|
||||
onClose();
|
||||
};
|
||||
const onAutoMergePRs = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS));
|
||||
onClose();
|
||||
};
|
||||
const onCleanDependencies = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES));
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ConversationPanelWrapper({
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-full w-full left-0 top-0 z-[9999] bg-black/80 rounded-xl",
|
||||
"absolute h-full w-full left-0 top-0 z-20 bg-black/80 rounded-xl",
|
||||
pathname === "/" && "bottom-0 top-0 md:top-3 md:bottom-3 h-auto",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ChatInterface } from "../../chat/chat-interface";
|
||||
|
||||
interface ChatInterfaceWrapperProps {
|
||||
@@ -8,16 +7,15 @@ interface ChatInterfaceWrapperProps {
|
||||
export function ChatInterfaceWrapper({
|
||||
isRightPanelShown,
|
||||
}: ChatInterfaceWrapperProps) {
|
||||
return (
|
||||
<div className="flex justify-center w-full h-full">
|
||||
<div
|
||||
className={cn(
|
||||
"w-full transition-all duration-300 ease-in-out",
|
||||
isRightPanelShown ? "max-w-4xl" : "max-w-6xl",
|
||||
)}
|
||||
>
|
||||
<ChatInterface />
|
||||
if (!isRightPanelShown) {
|
||||
return (
|
||||
<div className="flex justify-center w-full h-full">
|
||||
<div className="w-full max-w-[768px]">
|
||||
<ChatInterface />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return <ChatInterface />;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useWindowSize } from "@uidotdev/usehooks";
|
||||
import { RootState } from "#/store";
|
||||
import { MobileLayout } from "./mobile-layout";
|
||||
import { DesktopLayout } from "./desktop-layout";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export function ConversationMain() {
|
||||
const { width } = useWindowSize();
|
||||
const { isRightPanelShown } = useConversationStore();
|
||||
const isRightPanelShown = useSelector(
|
||||
(state: RootState) => state.conversation.isRightPanelShown,
|
||||
);
|
||||
|
||||
if (width && width <= 1024) {
|
||||
return <MobileLayout isRightPanelShown={isRightPanelShown} />;
|
||||
|
||||
@@ -1,64 +1,35 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { ChatInterfaceWrapper } from "./chat-interface-wrapper";
|
||||
import { ConversationTabContent } from "../conversation-tabs/conversation-tab-content/conversation-tab-content";
|
||||
import { ResizeHandle } from "../../../ui/resize-handle";
|
||||
import { useResizablePanels } from "#/hooks/use-resizable-panels";
|
||||
|
||||
interface DesktopLayoutProps {
|
||||
isRightPanelShown: boolean;
|
||||
}
|
||||
|
||||
export function DesktopLayout({ isRightPanelShown }: DesktopLayoutProps) {
|
||||
const { leftWidth, rightWidth, isDragging, containerRef, handleMouseDown } =
|
||||
useResizablePanels({
|
||||
defaultLeftWidth: 50,
|
||||
minLeftWidth: 30,
|
||||
maxLeftWidth: 80,
|
||||
storageKey: "desktop-layout-panel-width",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col overflow-hidden">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex flex-1 transition-all duration-300 ease-in-out overflow-hidden"
|
||||
style={{
|
||||
// Only apply smooth transitions when not dragging
|
||||
transitionProperty: isDragging ? "none" : "all",
|
||||
}}
|
||||
>
|
||||
{/* Left Panel (Chat) */}
|
||||
<div
|
||||
className="flex flex-col bg-base overflow-hidden transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
width: isRightPanelShown ? `${leftWidth}%` : "100%",
|
||||
transitionProperty: isDragging ? "none" : "all",
|
||||
}}
|
||||
>
|
||||
<ChatInterfaceWrapper isRightPanelShown={isRightPanelShown} />
|
||||
</div>
|
||||
|
||||
{/* Resize Handle */}
|
||||
{isRightPanelShown && <ResizeHandle onMouseDown={handleMouseDown} />}
|
||||
|
||||
{/* Right Panel */}
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-300 ease-in-out overflow-hidden",
|
||||
isRightPanelShown
|
||||
? "translate-x-0 opacity-100"
|
||||
: "w-0 translate-x-full opacity-0",
|
||||
)}
|
||||
style={{
|
||||
width: isRightPanelShown ? `${rightWidth}%` : "0%",
|
||||
transitionProperty: isDragging ? "opacity, transform" : "all",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-1 gap-3 min-w-max h-full">
|
||||
<ConversationTabContent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PanelGroup
|
||||
direction="horizontal"
|
||||
className="grow h-full min-h-0 min-w-0"
|
||||
autoSaveId="react-resizable-panels:layout"
|
||||
>
|
||||
<Panel minSize={30} maxSize={80} className="overflow-hidden bg-base">
|
||||
<ChatInterfaceWrapper isRightPanelShown={isRightPanelShown} />
|
||||
</Panel>
|
||||
{isRightPanelShown && (
|
||||
<>
|
||||
<PanelResizeHandle className="cursor-ew-resize" />
|
||||
<Panel
|
||||
minSize={20}
|
||||
maxSize={70}
|
||||
className="flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col flex-1 gap-3">
|
||||
<ConversationTabContent />
|
||||
</div>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,30 +8,20 @@ interface MobileLayoutProps {
|
||||
|
||||
export function MobileLayout({ isRightPanelShown }: MobileLayoutProps) {
|
||||
return (
|
||||
<div className="relative h-full flex flex-col overflow-hidden">
|
||||
{/* Chat area - shrinks when panel slides up */}
|
||||
<div className="flex flex-col gap-3 overflow-auto w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 bg-base overflow-hidden transition-all duration-300 ease-in-out",
|
||||
isRightPanelShown ? "flex-[0.6]" : "flex-1",
|
||||
"overflow-hidden w-full bg-base min-h-[600px]",
|
||||
!isRightPanelShown && "h-full",
|
||||
)}
|
||||
>
|
||||
<ChatInterface />
|
||||
</div>
|
||||
|
||||
{/* Bottom panel - slides up from bottom */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 left-0 right-0 transition-all duration-300 ease-in-out overflow-hidden",
|
||||
isRightPanelShown
|
||||
? "h-[40%] translate-y-0 opacity-100"
|
||||
: "h-0 translate-y-full opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="h-full flex flex-col gap-3 pt-2">
|
||||
{isRightPanelShown && (
|
||||
<div className="h-full w-full min-h-[494px] flex flex-col gap-3">
|
||||
<ConversationTabContent />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { lazy, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { ConversationLoading } from "../../conversation-loading";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { TabWrapper } from "./tab-wrapper";
|
||||
@@ -7,7 +9,6 @@ import { TabContainer } from "./tab-container";
|
||||
import { TabContentArea } from "./tab-content-area";
|
||||
import { ConversationTabTitle } from "../conversation-tab-title";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
// Lazy load all tab components
|
||||
const EditorTab = lazy(() => import("#/routes/changes-tab"));
|
||||
@@ -17,7 +18,12 @@ const ServedTab = lazy(() => import("#/routes/served-tab"));
|
||||
const VSCodeTab = lazy(() => import("#/routes/vscode-tab"));
|
||||
|
||||
export function ConversationTabContent() {
|
||||
const { selectedTab, shouldShownAgentLoading } = useConversationStore();
|
||||
const selectedTab = useSelector(
|
||||
(state: RootState) => state.conversation.selectedTab,
|
||||
);
|
||||
const { shouldShownAgentLoading } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import JupyterIcon from "#/icons/jupyter.svg?react";
|
||||
@@ -13,17 +14,21 @@ import { ChatActionTooltip } from "../../chat/chat-action-tooltip";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { VSCodeTooltipContent } from "./vscode-tooltip-content";
|
||||
import {
|
||||
useConversationStore,
|
||||
setHasRightPanelToggled,
|
||||
setSelectedTab,
|
||||
setIsRightPanelShown,
|
||||
type ConversationTab,
|
||||
} from "#/state/conversation-store";
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
export function ConversationTabs() {
|
||||
const {
|
||||
selectedTab,
|
||||
isRightPanelShown,
|
||||
setHasRightPanelToggled,
|
||||
setSelectedTab,
|
||||
} = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const selectedTab = useSelector(
|
||||
(state: RootState) => state.conversation.selectedTab,
|
||||
);
|
||||
const { isRightPanelShown } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
// Persist selectedTab and isRightPanelShown in localStorage
|
||||
const [persistedSelectedTab, setPersistedSelectedTab] =
|
||||
@@ -36,22 +41,18 @@ export function ConversationTabs() {
|
||||
useLocalStorage<boolean>("conversation-right-panel-shown", true);
|
||||
|
||||
const onTabChange = (value: ConversationTab | null) => {
|
||||
setSelectedTab(value);
|
||||
dispatch(setSelectedTab(value));
|
||||
// Persist the selected tab to localStorage
|
||||
setPersistedSelectedTab(value);
|
||||
};
|
||||
|
||||
// Initialize Zustand state from localStorage on component mount
|
||||
// Initialize Redux state from localStorage on component mount
|
||||
useEffect(() => {
|
||||
// Initialize selectedTab from localStorage if available
|
||||
setSelectedTab(persistedSelectedTab);
|
||||
setHasRightPanelToggled(persistedIsRightPanelShown);
|
||||
}, [
|
||||
setSelectedTab,
|
||||
setHasRightPanelToggled,
|
||||
persistedSelectedTab,
|
||||
persistedIsRightPanelShown,
|
||||
]);
|
||||
dispatch(setSelectedTab(persistedSelectedTab));
|
||||
dispatch(setIsRightPanelShown(persistedIsRightPanelShown));
|
||||
dispatch(setHasRightPanelToggled(persistedIsRightPanelShown));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePanelVisibilityChange = () => {
|
||||
@@ -71,13 +72,13 @@ export function ConversationTabs() {
|
||||
const onTabSelected = (tab: ConversationTab) => {
|
||||
if (selectedTab === tab && isRightPanelShown) {
|
||||
// If clicking the same active tab, close the drawer
|
||||
setHasRightPanelToggled(false);
|
||||
dispatch(setHasRightPanelToggled(false));
|
||||
setPersistedIsRightPanelShown(false);
|
||||
} else {
|
||||
// If clicking a different tab or drawer is closed, open drawer and select tab
|
||||
onTabChange(tab);
|
||||
if (!isRightPanelShown) {
|
||||
setHasRightPanelToggled(true);
|
||||
dispatch(setHasRightPanelToggled(true));
|
||||
setPersistedIsRightPanelShown(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Cell } from "#/state/jupyter-store";
|
||||
import { Cell } from "#/state/jupyter-slice";
|
||||
import { JupyterLine, parseCellContent } from "#/utils/parse-cell-content";
|
||||
import { JupytrerCellInput } from "./jupyter-cell-input";
|
||||
import { JupyterCellOutput } from "./jupyter-cell-output";
|
||||
|
||||
@@ -9,14 +9,13 @@ import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import JupyterLargeIcon from "#/icons/jupyter-large.svg?react";
|
||||
import { WaitingForRuntimeMessage } from "../chat/waiting-for-runtime-message";
|
||||
import { useJupyterStore } from "#/state/jupyter-store";
|
||||
|
||||
interface JupyterEditorProps {
|
||||
maxWidth: number;
|
||||
}
|
||||
|
||||
export function JupyterEditor({ maxWidth }: JupyterEditorProps) {
|
||||
const cells = useJupyterStore((state) => state.cells);
|
||||
const cells = useSelector((state: RootState) => state.jupyter?.cells ?? []);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const jupyterRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setAddMicroagentModalVisible,
|
||||
setSelectedRepository,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
interface MicroagentManagementAddMicroagentButtonProps {
|
||||
@@ -12,16 +17,16 @@ export function MicroagentManagementAddMicroagentButton({
|
||||
}: MicroagentManagementAddMicroagentButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
addMicroagentModalVisible,
|
||||
setAddMicroagentModalVisible,
|
||||
setSelectedRepository,
|
||||
} = useMicroagentManagementStore();
|
||||
const { addMicroagentModalVisible } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
setAddMicroagentModalVisible(!addMicroagentModalVisible);
|
||||
setSelectedRepository(repository);
|
||||
dispatch(setAddMicroagentModalVisible(!addMicroagentModalVisible));
|
||||
dispatch(setSelectedRepository(repository));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { MicroagentManagementSidebar } from "./microagent-management-sidebar";
|
||||
import { MicroagentManagementMain } from "./microagent-management-main";
|
||||
import { MicroagentManagementUpsertMicroagentModal } from "./microagent-management-upsert-microagent-modal";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import {
|
||||
setAddMicroagentModalVisible,
|
||||
setUpdateMicroagentModalVisible,
|
||||
setLearnThisRepoModalVisible,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { useCreateConversationAndSubscribeMultiple } from "#/hooks/use-create-conversation-and-subscribe-multiple";
|
||||
import {
|
||||
LearnThisRepoFormData,
|
||||
@@ -100,15 +106,14 @@ export function MicroagentManagementContent() {
|
||||
updateMicroagentModalVisible,
|
||||
selectedRepository,
|
||||
learnThisRepoModalVisible,
|
||||
setAddMicroagentModalVisible,
|
||||
setUpdateMicroagentModalVisible,
|
||||
setLearnThisRepoModalVisible,
|
||||
} = useMicroagentManagementStore();
|
||||
} = useSelector((state: RootState) => state.microagentManagement);
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { createConversationAndSubscribe, isPending } =
|
||||
useCreateConversationAndSubscribeMultiple();
|
||||
|
||||
@@ -125,9 +130,9 @@ export function MicroagentManagementContent() {
|
||||
|
||||
const hideUpsertMicroagentModal = (isUpdate: boolean = false) => {
|
||||
if (isUpdate) {
|
||||
setUpdateMicroagentModalVisible(false);
|
||||
dispatch(setUpdateMicroagentModalVisible(false));
|
||||
} else {
|
||||
setAddMicroagentModalVisible(false);
|
||||
dispatch(setAddMicroagentModalVisible(false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,7 +264,7 @@ export function MicroagentManagementContent() {
|
||||
};
|
||||
|
||||
const hideLearnThisRepoModal = () => {
|
||||
setLearnThisRepoModalVisible(false);
|
||||
dispatch(setLearnThisRepoModalVisible(false));
|
||||
};
|
||||
|
||||
const handleLearnThisRepoConfirm = (formData: LearnThisRepoFormData) => {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementConversationStopped() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementError() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FaCircleInfo } from "react-icons/fa6";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import XIcon from "#/icons/x.svg?react";
|
||||
import { cn, getRepoMdCreatePrompt } from "#/utils/utils";
|
||||
import { LearnThisRepoFormData } from "#/types/microagent-management";
|
||||
@@ -25,7 +26,9 @@ export function MicroagentManagementLearnThisRepoModal({
|
||||
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
const { selectedRepository } = useMicroagentManagementStore();
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setLearnThisRepoModalVisible,
|
||||
setSelectedRepository,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
interface MicroagentManagementLearnThisRepoProps {
|
||||
@@ -10,13 +14,12 @@ interface MicroagentManagementLearnThisRepoProps {
|
||||
export function MicroagentManagementLearnThisRepo({
|
||||
repository,
|
||||
}: MicroagentManagementLearnThisRepoProps) {
|
||||
const { setLearnThisRepoModalVisible, setSelectedRepository } =
|
||||
useMicroagentManagementStore();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () => {
|
||||
setLearnThisRepoModalVisible(true);
|
||||
setSelectedRepository(repository);
|
||||
dispatch(setLearnThisRepoModalVisible(true));
|
||||
dispatch(setSelectedRepository(repository));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { MicroagentManagementDefault } from "./microagent-management-default";
|
||||
import { MicroagentManagementOpeningPr } from "./microagent-management-opening-pr";
|
||||
import { MicroagentManagementReviewPr } from "./microagent-management-review-pr";
|
||||
@@ -7,7 +8,9 @@ import { MicroagentManagementError } from "./microagent-management-error";
|
||||
import { MicroagentManagementConversationStopped } from "./microagent-management-conversation-stopped";
|
||||
|
||||
export function MicroagentManagementMain() {
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent, conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setSelectedMicroagentItem,
|
||||
setSelectedRepository,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
@@ -20,11 +25,11 @@ export function MicroagentManagementMicroagentCard({
|
||||
}: MicroagentManagementMicroagentCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
selectedMicroagentItem,
|
||||
setSelectedMicroagentItem,
|
||||
setSelectedRepository,
|
||||
} = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
status: conversationStatus,
|
||||
@@ -78,18 +83,20 @@ export function MicroagentManagementMicroagentCard({
|
||||
}, [microagent, conversation, selectedMicroagentItem]);
|
||||
|
||||
const onMicroagentCardClicked = () => {
|
||||
setSelectedMicroagentItem(
|
||||
microagent
|
||||
? {
|
||||
microagent,
|
||||
conversation: undefined,
|
||||
}
|
||||
: {
|
||||
microagent: undefined,
|
||||
conversation,
|
||||
},
|
||||
dispatch(
|
||||
setSelectedMicroagentItem(
|
||||
microagent
|
||||
? {
|
||||
microagent,
|
||||
conversation: null,
|
||||
}
|
||||
: {
|
||||
microagent: null,
|
||||
conversation,
|
||||
},
|
||||
),
|
||||
);
|
||||
setSelectedRepository(repository);
|
||||
dispatch(setSelectedRepository(repository));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementOpeningPr() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentManagementMicroagentCard } from "./microagent-management-microagent-card";
|
||||
import { MicroagentManagementLearnThisRepo } from "./microagent-management-learn-this-repo";
|
||||
import { useRepositoryMicroagents } from "#/hooks/query/use-repository-microagents";
|
||||
import { useMicroagentManagementConversations } from "#/hooks/query/use-microagent-management-conversations";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import { setSelectedMicroagentItem } from "#/state/microagent-management-slice";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
@@ -17,8 +19,11 @@ interface MicroagentManagementRepoMicroagentsProps {
|
||||
export function MicroagentManagementRepoMicroagents({
|
||||
repository,
|
||||
}: MicroagentManagementRepoMicroagentsProps) {
|
||||
const { selectedMicroagentItem, setSelectedMicroagentItem } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -55,22 +60,26 @@ export function MicroagentManagementRepoMicroagents({
|
||||
conversation.conversation_id === selectedConversation.conversation_id,
|
||||
);
|
||||
if (latestSelectedConversation) {
|
||||
setSelectedMicroagentItem({
|
||||
microagent: undefined,
|
||||
conversation: latestSelectedConversation,
|
||||
});
|
||||
dispatch(
|
||||
setSelectedMicroagentItem({
|
||||
microagent: null,
|
||||
conversation: latestSelectedConversation,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [conversations]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
setSelectedMicroagentItem({
|
||||
microagent: undefined,
|
||||
conversation: undefined,
|
||||
});
|
||||
dispatch(
|
||||
setSelectedMicroagentItem({
|
||||
microagent: null,
|
||||
conversation: null,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setSelectedMicroagentItem],
|
||||
[],
|
||||
);
|
||||
|
||||
// Show loading only when both queries are loading
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { getProviderName, constructPullRequestUrl } from "#/utils/utils";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
export function MicroagentManagementReviewPr() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Tab, Tabs } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { MicroagentManagementRepositories } from "./microagent-management-repositories";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface MicroagentManagementSidebarTabsProps {
|
||||
isSearchLoading?: boolean;
|
||||
@@ -14,7 +15,7 @@ export function MicroagentManagementSidebarTabs({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { repositories, personalRepositories, organizationRepositories } =
|
||||
useMicroagentManagementStore();
|
||||
useSelector((state: RootState) => state.microagentManagement);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentManagementSidebarHeader } from "./microagent-management-sidebar-header";
|
||||
@@ -6,7 +7,11 @@ import { MicroagentManagementSidebarTabs } from "./microagent-management-sidebar
|
||||
import { useGitRepositories } from "#/hooks/query/use-git-repositories";
|
||||
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
|
||||
import { GitProviderDropdown } from "#/components/features/home/git-provider-dropdown";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { cn } from "#/utils/utils";
|
||||
@@ -30,11 +35,7 @@ export function MicroagentManagementSidebar({
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300);
|
||||
|
||||
const {
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
} = useMicroagentManagementStore();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -95,9 +96,9 @@ export function MicroagentManagementSidebar({
|
||||
|
||||
useEffect(() => {
|
||||
if (!filteredRepositories?.length) {
|
||||
setPersonalRepositories([]);
|
||||
setOrganizationRepositories([]);
|
||||
setRepositories([]);
|
||||
dispatch(setPersonalRepositories([]));
|
||||
dispatch(setOrganizationRepositories([]));
|
||||
dispatch(setRepositories([]));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,16 +121,10 @@ export function MicroagentManagementSidebar({
|
||||
}
|
||||
});
|
||||
|
||||
setPersonalRepositories(personalRepos);
|
||||
setOrganizationRepositories(organizationRepos);
|
||||
setRepositories(otherRepos);
|
||||
}, [
|
||||
filteredRepositories,
|
||||
selectedProvider,
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
]);
|
||||
dispatch(setPersonalRepositories(personalRepos));
|
||||
dispatch(setOrganizationRepositories(organizationRepos));
|
||||
dispatch(setRepositories(otherRepos));
|
||||
}, [filteredRepositories, selectedProvider, dispatch]);
|
||||
|
||||
// Handle scroll to bottom for pagination
|
||||
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FaCircleInfo } from "react-icons/fa6";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import XIcon from "#/icons/x.svg?react";
|
||||
import { cn, extractRepositoryInfo } from "#/utils/utils";
|
||||
import { BadgeInput } from "#/components/shared/inputs/badge-input";
|
||||
@@ -31,8 +32,13 @@ export function MicroagentManagementUpsertMicroagentModal({
|
||||
const [triggers, setTriggers] = useState<string[]>([]);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
const { selectedRepository, selectedMicroagentItem } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
@@ -7,15 +8,20 @@ import { code } from "../markdown/code";
|
||||
import { ul, ol } from "../markdown/list";
|
||||
import { paragraph } from "../markdown/paragraph";
|
||||
import { anchor } from "../markdown/anchor";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
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, selectedRepository } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RootState } from "#/store";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { getProviderName, constructMicroagentUrl } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { setUpdateMicroagentModalVisible } from "#/state/microagent-management-slice";
|
||||
|
||||
export function MicroagentManagementViewMicroagentHeader() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
selectedMicroagentItem,
|
||||
selectedRepository,
|
||||
setUpdateMicroagentModalVisible,
|
||||
} = useMicroagentManagementStore();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
@@ -26,7 +32,7 @@ export function MicroagentManagementViewMicroagentHeader() {
|
||||
);
|
||||
|
||||
const handleLearnSomethingNew = () => {
|
||||
setUpdateMicroagentModalVisible(true);
|
||||
dispatch(setUpdateMicroagentModalVisible(true));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { MicroagentManagementViewMicroagentHeader } from "./microagent-management-view-microagent-header";
|
||||
import { MicroagentManagementViewMicroagentContent } from "./microagent-management-view-microagent-content";
|
||||
|
||||
export function MicroagentManagementViewMicroagent() {
|
||||
const { selectedMicroagentItem, selectedRepository } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -121,22 +120,6 @@ export function MCPServerForm({
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateTimeout = (timeoutStr: string): string | null => {
|
||||
if (!timeoutStr.trim()) return null; // Optional field
|
||||
|
||||
const timeout = parseInt(timeoutStr.trim(), 10);
|
||||
if (Number.isNaN(timeout)) {
|
||||
return t(I18nKey.SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER);
|
||||
}
|
||||
if (timeout <= 0) {
|
||||
return t(I18nKey.SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE);
|
||||
}
|
||||
if (timeout > 3600) {
|
||||
return t(I18nKey.SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateStdioServer = (formData: FormData): string | null => {
|
||||
const name = formData.get("name")?.toString().trim() || "";
|
||||
const command = formData.get("command")?.toString().trim() || "";
|
||||
@@ -165,14 +148,6 @@ export function MCPServerForm({
|
||||
if (urlError) return urlError;
|
||||
const urlDupError = validateUrlUniqueness(url);
|
||||
if (urlDupError) return urlDupError;
|
||||
|
||||
// Validate timeout for SHTTP servers only
|
||||
if (serverType === "shttp") {
|
||||
const timeoutStr = formData.get("timeout")?.toString() || "";
|
||||
const timeoutError = validateTimeout(timeoutStr);
|
||||
if (timeoutError) return timeoutError;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -228,23 +203,12 @@ export function MCPServerForm({
|
||||
if (serverType === "sse" || serverType === "shttp") {
|
||||
const url = formData.get("url")?.toString().trim();
|
||||
const apiKey = formData.get("api_key")?.toString().trim();
|
||||
const timeoutStr = formData.get("timeout")?.toString().trim();
|
||||
|
||||
const serverConfig: MCPServerConfig = {
|
||||
onSubmit({
|
||||
...baseConfig,
|
||||
url: url!,
|
||||
...(apiKey && { api_key: apiKey }),
|
||||
};
|
||||
|
||||
// Only add timeout for SHTTP servers
|
||||
if (serverType === "shttp" && timeoutStr) {
|
||||
const timeoutValue = parseInt(timeoutStr, 10);
|
||||
if (!Number.isNaN(timeoutValue)) {
|
||||
serverConfig.timeout = timeoutValue;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(serverConfig);
|
||||
});
|
||||
} else if (serverType === "stdio") {
|
||||
const name = formData.get("name")?.toString().trim();
|
||||
const command = formData.get("command")?.toString().trim();
|
||||
@@ -319,21 +283,6 @@ export function MCPServerForm({
|
||||
defaultValue={server?.api_key || ""}
|
||||
placeholder={t(I18nKey.SETTINGS$MCP_API_KEY_PLACEHOLDER)}
|
||||
/>
|
||||
|
||||
{serverType === "shttp" && (
|
||||
<SettingsInput
|
||||
testId="timeout-input"
|
||||
name="timeout"
|
||||
type="number"
|
||||
label="Timeout (seconds)"
|
||||
className="w-full max-w-[680px]"
|
||||
showOptionalTag
|
||||
defaultValue={server?.timeout?.toString() || ""}
|
||||
placeholder="60"
|
||||
min={1}
|
||||
max={3600}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
||||
@@ -8,7 +8,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
@@ -6,19 +7,19 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { ActionTooltip } from "../action-tooltip";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import { RiskAlert } from "#/components/shared/risk-alert";
|
||||
import WarningIcon from "#/icons/u-warning.svg?react";
|
||||
import { useEventMessageStore } from "#/stores/event-message-store";
|
||||
import { RootState } from "#/store";
|
||||
import { addSubmittedEventId } from "#/state/event-message-slice";
|
||||
|
||||
export function ConfirmationButtons() {
|
||||
const submittedEventIds = useEventMessageStore(
|
||||
(state) => state.submittedEventIds,
|
||||
);
|
||||
const addSubmittedEventId = useEventMessageStore(
|
||||
(state) => state.addSubmittedEventId,
|
||||
const submittedEventIds = useSelector(
|
||||
(state: RootState) => state.eventMessage.submittedEventIds,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { send, parsedEvents } = useWsClient();
|
||||
@@ -39,10 +40,10 @@ export function ConfirmationButtons() {
|
||||
return;
|
||||
}
|
||||
|
||||
addSubmittedEventId(awaitingAction.id);
|
||||
dispatch(addSubmittedEventId(awaitingAction.id));
|
||||
send(generateAgentStateChangeEvent(state));
|
||||
},
|
||||
[send, addSubmittedEventId],
|
||||
[send],
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ResizeHandleProps {
|
||||
onMouseDown: (e: React.MouseEvent) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ResizeHandle({ onMouseDown, className }: ResizeHandleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn("relative w-1 bg-transparent cursor-ew-resize", className)}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
{/* Visual indicator */}
|
||||
<div className="absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2" />
|
||||
|
||||
{/* Larger hit area for easier dragging */}
|
||||
<div className="absolute inset-y-0 -left-1 -right-1" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,33 +30,19 @@ export const useChatInputEvents = (
|
||||
ensureCursorVisible(chatInputRef.current);
|
||||
}, [smartResize, chatInputRef]);
|
||||
|
||||
// Handle paste events to clean up formatting and handle files
|
||||
// Handle paste events to clean up formatting
|
||||
const handlePaste = useCallback(
|
||||
(e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Check if there are files in the clipboard
|
||||
const files = Array.from(e.clipboardData.files);
|
||||
const hasFiles = files.length > 0;
|
||||
|
||||
if (hasFiles) {
|
||||
// Handle file paste - let the file handling system process the files
|
||||
// We'll trigger a custom event that the file handling system can listen to
|
||||
const customEvent = new CustomEvent("pasteFiles", {
|
||||
detail: { files },
|
||||
});
|
||||
document.dispatchEvent(customEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle text paste as before
|
||||
// Get plain text from clipboard
|
||||
const text = e.clipboardData.getData("text/plain");
|
||||
if (text) {
|
||||
// Insert plain text
|
||||
document.execCommand("insertText", false, text);
|
||||
// Trigger resize
|
||||
setTimeout(smartResize, 0);
|
||||
}
|
||||
|
||||
// Insert plain text
|
||||
document.execCommand("insertText", false, text);
|
||||
|
||||
// Trigger resize
|
||||
setTimeout(smartResize, 0);
|
||||
},
|
||||
[smartResize],
|
||||
);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useRef, useCallback, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
setMessageToSend,
|
||||
setIsRightPanelShown,
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
import {
|
||||
isContentEmpty,
|
||||
clearEmptyContent,
|
||||
getTextContent,
|
||||
} from "#/components/features/chat/utils/chat-input.utils";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
/**
|
||||
* Hook for managing chat input content logic
|
||||
@@ -12,21 +17,20 @@ import { useConversationStore } from "#/state/conversation-store";
|
||||
export const useChatInputLogic = () => {
|
||||
const chatInputRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
messageToSend,
|
||||
hasRightPanelToggled,
|
||||
setMessageToSend,
|
||||
setIsRightPanelShown,
|
||||
} = useConversationStore();
|
||||
const { messageToSend, hasRightPanelToggled } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Save current input value when drawer state changes
|
||||
useEffect(() => {
|
||||
if (chatInputRef.current) {
|
||||
const currentText = getTextContent(chatInputRef.current);
|
||||
setMessageToSend(currentText);
|
||||
setIsRightPanelShown(hasRightPanelToggled);
|
||||
dispatch(setMessageToSend(currentText));
|
||||
dispatch(setIsRightPanelShown(hasRightPanelToggled));
|
||||
}
|
||||
}, [hasRightPanelToggled, setMessageToSend, setIsRightPanelShown]);
|
||||
}, [hasRightPanelToggled, dispatch]);
|
||||
|
||||
// Helper function to check if contentEditable is truly empty
|
||||
const checkIsContentEmpty = useCallback(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback, useState, useEffect } from "react";
|
||||
import React, { useRef, useCallback, useState } from "react";
|
||||
|
||||
interface UseFileHandlingReturn {
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
@@ -31,25 +31,6 @@ export const useFileHandling = (
|
||||
[onFilesPaste],
|
||||
);
|
||||
|
||||
// Listen for paste events with files
|
||||
useEffect(() => {
|
||||
const handlePasteFiles = (event: CustomEvent) => {
|
||||
const files = event.detail.files as File[];
|
||||
if (files && files.length > 0) {
|
||||
addFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("pasteFiles", handlePasteFiles as EventListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"pasteFiles",
|
||||
handlePasteFiles as EventListener,
|
||||
);
|
||||
};
|
||||
}, [addFiles]);
|
||||
|
||||
// File icon click handler
|
||||
const handleFileIconClick = useCallback((isDisabled: boolean) => {
|
||||
if (!isDisabled && fileInputRef.current) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useAutoResize } from "#/hooks/use-auto-resize";
|
||||
import { CHAT_INPUT } from "#/utils/constants";
|
||||
import {
|
||||
IMessageToSend,
|
||||
useConversationStore,
|
||||
} from "#/state/conversation-store";
|
||||
setShouldHideSuggestions,
|
||||
} from "#/state/conversation-slice";
|
||||
import { CHAT_INPUT } from "#/utils/constants";
|
||||
|
||||
/**
|
||||
* Hook for managing grip resize functionality
|
||||
@@ -13,11 +14,11 @@ export const useGripResize = (
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>,
|
||||
messageToSend: IMessageToSend | null,
|
||||
) => {
|
||||
const gripRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [isGripVisible, setIsGripVisible] = useState(false);
|
||||
|
||||
const { setShouldHideSuggestions } = useConversationStore();
|
||||
|
||||
const gripRef = useRef<HTMLDivElement | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Drag state management callbacks
|
||||
const handleDragStart = useCallback(() => {
|
||||
@@ -47,9 +48,9 @@ export const useGripResize = (
|
||||
(height: number) => {
|
||||
// Hide suggestions when input height exceeds the threshold
|
||||
const shouldHideChatSuggestions = height > CHAT_INPUT.HEIGHT_THRESHOLD;
|
||||
setShouldHideSuggestions(shouldHideChatSuggestions);
|
||||
dispatch(setShouldHideSuggestions(shouldHideChatSuggestions));
|
||||
},
|
||||
[setShouldHideSuggestions],
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// Use the auto-resize hook with height change callback
|
||||
|
||||
@@ -10,7 +10,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -50,7 +49,6 @@ export function useAddMcpServer() {
|
||||
const shttpServer: MCPSHTTPServer = {
|
||||
url: server.url!,
|
||||
...(server.api_key && { api_key: server.api_key }),
|
||||
...(server.timeout !== undefined && { timeout: server.timeout }),
|
||||
};
|
||||
newConfig.shttp_servers.push(shttpServer);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -52,7 +51,6 @@ export function useUpdateMcpServer() {
|
||||
const shttpServer: MCPSHTTPServer = {
|
||||
url: server.url!,
|
||||
...(server.api_key && { api_key: server.api_key }),
|
||||
...(server.timeout !== undefined && { timeout: server.timeout }),
|
||||
};
|
||||
newConfig.shttp_servers[index] = shttpServer;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, RefObject } from "react";
|
||||
import { IMessageToSend } from "#/state/conversation-store";
|
||||
import { IMessageToSend } from "#/state/conversation-slice";
|
||||
import { useDragResize } from "./use-drag-resize";
|
||||
|
||||
// Constants
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
|
||||
interface UseResizablePanelsOptions {
|
||||
defaultLeftWidth?: number;
|
||||
minLeftWidth?: number;
|
||||
maxLeftWidth?: number;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
export function useResizablePanels({
|
||||
defaultLeftWidth = 50,
|
||||
minLeftWidth = 30,
|
||||
maxLeftWidth = 80,
|
||||
storageKey = "desktop-layout-panel-width",
|
||||
}: UseResizablePanelsOptions = {}) {
|
||||
const [persistedWidth, setPersistedWidth] = useLocalStorage<number>(
|
||||
storageKey,
|
||||
defaultLeftWidth,
|
||||
);
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState(persistedWidth);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const clampWidth = useCallback(
|
||||
(width: number) => Math.max(minLeftWidth, Math.min(maxLeftWidth, width)),
|
||||
[minLeftWidth, maxLeftWidth],
|
||||
);
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isDragging || !containerRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const mouseX = e.clientX - containerRect.left;
|
||||
const containerWidth = containerRect.width;
|
||||
const newLeftWidth = (mouseX / containerWidth) * 100;
|
||||
|
||||
const clampedWidth = clampWidth(newLeftWidth);
|
||||
setLeftWidth(clampedWidth);
|
||||
},
|
||||
[isDragging, clampWidth],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (isDragging) {
|
||||
setIsDragging(false);
|
||||
setPersistedWidth(leftWidth);
|
||||
}
|
||||
}, [isDragging, leftWidth, setPersistedWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "ew-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isDragging) {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
};
|
||||
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||
|
||||
const rightWidth = 100 - leftWidth;
|
||||
|
||||
return {
|
||||
leftWidth,
|
||||
rightWidth,
|
||||
isDragging,
|
||||
containerRef,
|
||||
handleMouseDown,
|
||||
};
|
||||
}
|
||||
@@ -806,9 +806,6 @@ export enum I18nKey {
|
||||
SETTINGS$MCP_ERROR_COMMAND_NO_SPACES = "SETTINGS$MCP_ERROR_COMMAND_NO_SPACES",
|
||||
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
|
||||
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
|
||||
SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER = "SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER",
|
||||
SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE = "SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE",
|
||||
SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED = "SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED",
|
||||
SETTINGS$MCP_SERVER_TYPE = "SETTINGS$MCP_SERVER_TYPE",
|
||||
SETTINGS$MCP_API_KEY_PLACEHOLDER = "SETTINGS$MCP_API_KEY_PLACEHOLDER",
|
||||
SETTINGS$MCP_COMMAND_ARGUMENTS = "SETTINGS$MCP_COMMAND_ARGUMENTS",
|
||||
|
||||
@@ -12895,54 +12895,6 @@
|
||||
"de": "Environment variables must follow KEY=value format",
|
||||
"uk": "Environment variables must follow KEY=value format"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER": {
|
||||
"en": "Timeout must be a valid number",
|
||||
"ja": "タイムアウトは有効な数値である必要があります",
|
||||
"zh-CN": "超时必须是有效数字",
|
||||
"zh-TW": "超時必須是有效數字",
|
||||
"ko-KR": "타임아웃은 유효한 숫자여야 합니다",
|
||||
"no": "Timeout må være et gyldig tall",
|
||||
"it": "Il timeout deve essere un numero valido",
|
||||
"pt": "O timeout deve ser um número válido",
|
||||
"es": "El timeout debe ser un número válido",
|
||||
"ar": "يجب أن يكون المهلة الزمنية رقمًا صالحًا",
|
||||
"fr": "Le timeout doit être un nombre valide",
|
||||
"tr": "Zaman aşımı geçerli bir sayı olmalıdır",
|
||||
"de": "Timeout muss eine gültige Zahl sein",
|
||||
"uk": "Таймаут повинен бути дійсним числом"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE": {
|
||||
"en": "Timeout must be positive",
|
||||
"ja": "タイムアウトは正の値である必要があります",
|
||||
"zh-CN": "超时必须为正数",
|
||||
"zh-TW": "超時必須為正數",
|
||||
"ko-KR": "타임아웃은 양수여야 합니다",
|
||||
"no": "Timeout må være positiv",
|
||||
"it": "Il timeout deve essere positivo",
|
||||
"pt": "O timeout deve ser positivo",
|
||||
"es": "El timeout debe ser positivo",
|
||||
"ar": "يجب أن تكون المهلة الزمنية موجبة",
|
||||
"fr": "Le timeout doit être positif",
|
||||
"tr": "Zaman aşımı pozitif olmalıdır",
|
||||
"de": "Timeout muss positiv sein",
|
||||
"uk": "Таймаут повинен бути позитивним"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED": {
|
||||
"en": "Timeout cannot exceed 3600 seconds (1 hour)",
|
||||
"ja": "タイムアウトは3600秒(1時間)を超えることはできません",
|
||||
"zh-CN": "超时不能超过3600秒(1小时)",
|
||||
"zh-TW": "超時不能超過3600秒(1小時)",
|
||||
"ko-KR": "타임아웃은 3600초(1시간)을 초과할 수 없습니다",
|
||||
"no": "Timeout kan ikke overstige 3600 sekunder (1 time)",
|
||||
"it": "Il timeout non può superare 3600 secondi (1 ora)",
|
||||
"pt": "O timeout não pode exceder 3600 segundos (1 hora)",
|
||||
"es": "El timeout no puede exceder 3600 segundos (1 hora)",
|
||||
"ar": "لا يمكن أن تتجاوز المهلة الزمنية 3600 ثانية (ساعة واحدة)",
|
||||
"fr": "Le timeout ne peut pas dépasser 3600 secondes (1 heure)",
|
||||
"tr": "Zaman aşımı 3600 saniyeyi (1 saat) aşamaz",
|
||||
"de": "Timeout kann 3600 Sekunden (1 Stunde) nicht überschreiten",
|
||||
"uk": "Таймаут не може перевищувати 3600 секунд (1 година)"
|
||||
},
|
||||
"SETTINGS$MCP_SERVER_TYPE": {
|
||||
"en": "Server Type",
|
||||
"ja": "サーバータイプ",
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useEffectOnce } from "#/hooks/use-effect-once";
|
||||
import { useJupyterStore } from "#/state/jupyter-store";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { clearJupyter } from "#/state/jupyter-slice";
|
||||
import { resetConversationState } from "#/state/conversation-slice";
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
@@ -38,11 +38,9 @@ function AppContent() {
|
||||
const { mutate: startConversation } = useStartConversation();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const { providers } = useUserProviders();
|
||||
const { resetConversationState } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const clearTerminal = useCommandStore((state) => state.clearTerminal);
|
||||
const clearJupyter = useJupyterStore((state) => state.clearJupyter);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch batch feedback data when conversation is loaded
|
||||
@@ -87,15 +85,15 @@ function AppContent() {
|
||||
|
||||
React.useEffect(() => {
|
||||
clearTerminal();
|
||||
clearJupyter();
|
||||
resetConversationState();
|
||||
dispatch(clearJupyter());
|
||||
dispatch(resetConversationState());
|
||||
dispatch(setCurrentAgentState(AgentState.LOADING));
|
||||
}, [conversationId, clearTerminal, resetConversationState]);
|
||||
}, [conversationId, clearTerminal]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
clearTerminal();
|
||||
clearJupyter();
|
||||
resetConversationState();
|
||||
dispatch(clearJupyter());
|
||||
dispatch(resetConversationState());
|
||||
dispatch(setCurrentAgentState(AgentState.LOADING));
|
||||
});
|
||||
|
||||
@@ -112,7 +110,9 @@ function AppContent() {
|
||||
<ConversationTabs />
|
||||
</div>
|
||||
|
||||
<ConversationMain />
|
||||
<div className="flex h-full overflow-auto">
|
||||
<ConversationMain />
|
||||
</div>
|
||||
</div>
|
||||
</EventHandler>
|
||||
</ConversationSubscriptionsProvider>
|
||||
|
||||
@@ -20,7 +20,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -68,7 +67,6 @@ function MCPSettingsScreen() {
|
||||
type: "shttp" as const,
|
||||
url: typeof server === "string" ? server : server.url,
|
||||
api_key: typeof server === "object" ? server.api_key : undefined,
|
||||
timeout: typeof server === "object" ? server.timeout : undefined,
|
||||
})),
|
||||
];
|
||||
|
||||
|
||||