Compare commits

..

24 Commits

Author SHA1 Message Date
chuckbutkus 7c556d6396 Merge branch 'main' into chuck-build 2025-09-23 14:25:16 -04:00
openhands 8bb5aa21b9 test 2025-09-23 14:19:20 -04:00
openhands 08096db29f test 2025-09-18 22:50:21 -04:00
openhands b2b6ddf90c test 2025-09-18 22:24:35 -04:00
openhands 87fe36d811 test 2025-09-18 21:44:34 -04:00
openhands 39d255d313 test 2025-09-18 21:27:03 -04:00
openhands e334b67f21 Add logging 2025-09-18 20:48:24 -04:00
chuckbutkus d5c02bf87b Merge branch 'main' into allow-custom-user 2025-09-17 22:43:30 -04:00
openhands 14a4664fe8 Make su commands optional 2025-09-17 22:40:21 -04:00
chuckbutkus 3a7df33acf Merge branch 'main' into test-user 2025-09-17 14:02:52 -04:00
chuckbutkus 69fddecc7f Merge branch 'main' into test-user 2025-09-07 21:55:39 -04:00
Chuck Butkus 3afe5ccee5 Add Logging 2025-09-05 20:52:48 -04:00
chuckbutkus 3d5a8dcf5a Merge branch 'main' into test-user 2025-09-05 14:20:10 -04:00
Chuck Butkus 2ee1abe22c Lint fix 2025-09-05 13:16:03 -04:00
Chuck Butkus 148940f553 Added logging around alive checks 2025-09-05 11:10:57 -04:00
Chuck Butkus 1f09296136 Fix username checks 2025-09-03 21:40:13 -04:00
Chuck Butkus 70e5d12ba9 Revert "Change to a non-login shell"
This reverts commit bcb3160d95.
2025-08-29 01:48:47 -04:00
Chuck Butkus bcb3160d95 Change to a non-login shell 2025-08-29 01:37:02 -04:00
Chuck Butkus 174c691744 Update 2025-08-28 02:25:05 -04:00
Chuck Butkus af34d446e9 Remove vscode username restriction 2025-08-28 02:22:27 -04:00
Chuck Butkus 6604924f76 Fix bash username 2025-08-28 02:21:41 -04:00
chuckbutkus b2def1e438 Merge branch 'main' into test-user 2025-08-27 23:33:45 -04:00
Chuck Butkus 2b8e47aca9 Add runtime user env vars 2025-08-27 23:02:39 -04:00
Chuck Butkus dba8b28824 Logging 2025-08-27 21:30:47 -04:00
135 changed files with 2727 additions and 3194 deletions
+6 -77
View File
@@ -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
+7 -54
View File
@@ -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
-41
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 144 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

+12 -13
View File
@@ -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!
![Connect Repo](/static/img/connect-repo.png)
## 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!
![Connect Repo](/static/img/connect-repo-no-github.png)
## Next Steps
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
+7 -4
View File
@@ -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 Key Generation](/static/img/api-key-generation.png)
## API Usage
+10 -25
View File
@@ -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
+9 -16
View File
@@ -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!
![Connect Repo](/static/img/connect-repo.png)
## 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
+6 -8
View File
@@ -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!
![Connect Repo](/static/img/connect-repo.png)
![Connect Repo](/static/img/connect-repo-no-github.png)
## Using Tokens with Reduced Scopes
OpenHands requests an API-scoped token during OAuth authentication. By default, this token is provided to the agent.
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default
token assigned to the agent. While the high-permission API token is still requested and used for other components of
the application (e.g. opening merge requests), the agent will not have access to it.
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.
+1 -3
View File
@@ -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
-46
View File
@@ -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.
+23 -117
View File
@@ -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"
-5
View File
@@ -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/*" ]
+1
View File
@@ -0,0 +1 @@
test
+12 -20
View File
@@ -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");
+4 -8
View File
@@ -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>,
);
+1025 -709
View File
File diff suppressed because it is too large Load Diff
+22 -21
View File
@@ -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",
+1 -1
View File
@@ -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],
);
+14 -10
View File
@@ -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 -20
View File
@@ -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) {
+9 -8
View File
@@ -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 -1
View File
@@ -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,
};
}
-3
View File
@@ -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",
-48
View File
@@ -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": "サーバータイプ",
+10 -10
View File
@@ -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>
-2
View File
@@ -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,
})),
];

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