mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 497fd4a02c | |||
| 7fac7d6dd0 | |||
| 4155b8f801 | |||
| e712b013f9 | |||
| 29b137e9b1 | |||
| 1292f0c2ea | |||
| da8c946078 | |||
| 43cef1f969 | |||
| ab661b485b | |||
| 9632914bf0 | |||
| 3db780ef93 | |||
| 43c16516e8 |
@@ -34,10 +34,6 @@ on:
|
||||
type: string
|
||||
default: ""
|
||||
description: "Custom sandbox env"
|
||||
runner:
|
||||
required: false
|
||||
type: string
|
||||
default: "ubuntu-latest"
|
||||
secrets:
|
||||
LLM_MODEL:
|
||||
required: false
|
||||
@@ -83,7 +79,7 @@ jobs:
|
||||
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER')
|
||||
)
|
||||
)
|
||||
runs-on: "${{ inputs.runner }}"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Running OpenHands pre-commit hook..."
|
||||
|
||||
# Store the exit code to return at the end
|
||||
# This allows us to be additive to existing pre-commit hooks
|
||||
EXIT_CODE=0
|
||||
|
||||
# Check if frontend directory has changed
|
||||
frontend_changes=$(git diff --cached --name-only | grep "^frontend/")
|
||||
if [ -n "$frontend_changes" ]; then
|
||||
echo "Frontend changes detected. Running frontend checks..."
|
||||
|
||||
# Check if frontend directory exists
|
||||
if [ -d "frontend" ]; then
|
||||
# Change to frontend directory
|
||||
cd frontend || exit 1
|
||||
|
||||
# Run lint:fix
|
||||
echo "Running npm lint:fix..."
|
||||
npm run lint:fix
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Frontend linting failed. Please fix the issues before committing."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
# Run build
|
||||
echo "Running npm build..."
|
||||
npm run build
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Frontend build failed. Please fix the issues before committing."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
echo "Running npm test..."
|
||||
npm test
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Frontend tests failed. Please fix the failing tests before committing."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
# Return to the original directory
|
||||
cd ..
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "Frontend checks passed!"
|
||||
fi
|
||||
else
|
||||
echo "Frontend directory not found. Skipping frontend checks."
|
||||
fi
|
||||
else
|
||||
echo "No frontend changes detected. Skipping frontend checks."
|
||||
fi
|
||||
|
||||
# Run any existing pre-commit hooks that might have been installed by the user
|
||||
# This makes our hook additive rather than replacing existing hooks
|
||||
if [ -f ".git/hooks/pre-commit.local" ]; then
|
||||
echo "Running existing pre-commit hooks..."
|
||||
bash .git/hooks/pre-commit.local
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Existing pre-commit hooks failed."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "All pre-commit checks passed!"
|
||||
else
|
||||
echo "Some pre-commit checks failed. Please fix the issues before committing."
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
@@ -2,11 +2,4 @@
|
||||
|
||||
echo "Setting up the environment..."
|
||||
|
||||
# Install pre-commit package
|
||||
python -m pip install pre-commit
|
||||
|
||||
# Install pre-commit hooks if .git directory exists
|
||||
if [ -d ".git" ]; then
|
||||
echo "Installing pre-commit hooks..."
|
||||
pre-commit install
|
||||
fi
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ These Slack and Discord etiquette guidelines are designed to foster an inclusive
|
||||
- Use threads for specific discussions to keep channels organized and easier to follow.
|
||||
- Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize disruptions.
|
||||
- Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond.
|
||||
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions, [discord - #general](https://discord.com/channels/1222935860639563850/1222935861386018885)).
|
||||
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://app.slack.com/client/T06P212QSEA/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions, [discord - #general](https://discord.com/channels/1222935860639563850/1222935861386018885)).
|
||||
- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to provide context.
|
||||
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the matter is sensitive or private.
|
||||
- Always adhere to [our standards](https://github.com/All-Hands-AI/OpenHands/blob/main/CODE_OF_CONDUCT.md#our-standards) to ensure a welcoming and collaborative environment.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
|
||||
<br/>
|
||||
@@ -99,19 +99,12 @@ check out our [documentation](https://docs.all-hands.dev/modules/usage/getting-s
|
||||
There you'll find resources on how to use different LLM providers,
|
||||
troubleshooting resources, and advanced configuration options.
|
||||
|
||||
### Custom Scripts
|
||||
|
||||
OpenHands supports custom scripts that run at different points in the runtime lifecycle:
|
||||
|
||||
- **setup.sh**: Place this script in the `.openhands` directory of your repository to run custom setup commands when the runtime initializes.
|
||||
- **pre-commit.sh**: Place this script in the `.openhands` directory to add a custom git pre-commit hook that runs before each commit. This can be used to enforce code quality standards, run tests, or perform other checks before allowing commits.
|
||||
|
||||
## 🤝 How to Join the Community
|
||||
|
||||
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
|
||||
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
|
||||
|
||||
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - Here we talk about research, architecture, and future development.
|
||||
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw) - Here we talk about research, architecture, and future development.
|
||||
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
|
||||
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# MCP Server Configuration
|
||||
# This configuration file adds the MCP server to the OpenHands configuration
|
||||
|
||||
# Include the MCP server in the configuration
|
||||
[mcp]
|
||||
# List of MCP SSE servers
|
||||
sse_servers = [
|
||||
{
|
||||
# The URL of the MCP server
|
||||
url = "http://localhost:12000/mcp",
|
||||
# Optional API key for authentication (not required for local development)
|
||||
api_key = ""
|
||||
}
|
||||
]
|
||||
|
||||
# List of MCP stdio servers (these will be started by the runtime)
|
||||
stdio_servers = []
|
||||
@@ -316,10 +316,6 @@ llm_config = 'gpt3'
|
||||
# Additional Docker runtime kwargs
|
||||
#docker_runtime_kwargs = {}
|
||||
|
||||
# Specific port to use for VSCode. If not set, a random port will be chosen.
|
||||
# Useful when deploying OpenHands in a remote machine where you need to expose a specific port.
|
||||
#vscode_port = 41234
|
||||
|
||||
#################################### Security ###################################
|
||||
# Configuration for security features
|
||||
##############################################################################
|
||||
|
||||
@@ -70,6 +70,7 @@ ENV VIRTUAL_ENV=/app/.venv \
|
||||
PYTHONPATH='/app'
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
RUN playwright install --with-deps chromium
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
|
||||
|
||||
@@ -26,6 +26,10 @@ fi
|
||||
if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
|
||||
echo "Running OpenHands as root"
|
||||
export RUN_AS_OPENHANDS=false
|
||||
mkdir -p /root/.cache/ms-playwright/
|
||||
if [ -d "/home/openhands/.cache/ms-playwright/" ]; then
|
||||
mv /home/openhands/.cache/ms-playwright/ /root/.cache/
|
||||
fi
|
||||
"$@"
|
||||
else
|
||||
echo "Setting up enduser with id $SANDBOX_USER_ID"
|
||||
@@ -54,6 +58,10 @@ else
|
||||
fi
|
||||
|
||||
mkdir -p /home/enduser/.cache/huggingface/hub/
|
||||
mkdir -p /home/enduser/.cache/ms-playwright/
|
||||
if [ -d "/home/openhands/.cache/ms-playwright/" ]; then
|
||||
mv /home/openhands/.cache/ms-playwright/ /home/enduser/.cache/
|
||||
fi
|
||||
|
||||
usermod -aG $DOCKER_SOCKET_GID enduser
|
||||
echo "Running as enduser"
|
||||
|
||||
@@ -26,7 +26,7 @@ repos:
|
||||
- id: ruff
|
||||
entry: ruff check --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
args: [--fix, --unsafe-fixes]
|
||||
args: [--fix]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
entry: ruff format --config dev_config/python/ruff.toml
|
||||
|
||||
@@ -7,9 +7,6 @@ select = [
|
||||
"Q",
|
||||
"B",
|
||||
"ASYNC",
|
||||
"UP006", # Use `list` instead of `List` for annotations
|
||||
"UP007", # Use `X | Y` instead of `Union[X, Y]`
|
||||
"UP008", # Use `X | None` instead of `Optional[X]`
|
||||
]
|
||||
|
||||
ignore = [
|
||||
|
||||
@@ -42,7 +42,7 @@ Explorez le code source d'OpenHands sur [GitHub](https://github.com/All-Hands-AI
|
||||
/>
|
||||
</a>
|
||||
<br></br>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A">
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw">
|
||||
<img
|
||||
src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge"
|
||||
alt="Join our Slack community"
|
||||
|
||||
@@ -42,7 +42,7 @@ OpenHandsのソースコードを[GitHub](https://github.com/All-Hands-AI/OpenHa
|
||||
/>
|
||||
</a>
|
||||
<br></br>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A">
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw">
|
||||
<img
|
||||
src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge"
|
||||
alt="Slackコミュニティに参加"
|
||||
|
||||
@@ -42,7 +42,7 @@ OpenHands 是一个**自主 AI 软件工程师**,能够执行复杂的工程
|
||||
/>
|
||||
</a>
|
||||
<br></br>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A">
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw">
|
||||
<img
|
||||
src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge"
|
||||
alt="Join our Slack community"
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# MCP Server for GitHub PR and GitLab MR Creation
|
||||
|
||||
This document describes the Model Context Protocol (MCP) server implementation in OpenHands that enables creating pull requests on GitHub and merge requests on GitLab directly from the chat interface.
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP server provides a standardized interface for creating pull requests and merge requests using the JSON-RPC 2.0 protocol. It integrates with OpenHands' existing GitHub and GitLab clients to handle authentication and API calls.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the core MCP protocol using JSON-RPC 2.0
|
||||
- Provides session management for MCP clients
|
||||
- Exposes tools for creating pull requests on GitHub and merge requests on GitLab
|
||||
- Integrates with OpenHands' existing GitHub and GitLab clients
|
||||
- Follows the MCP specification for capability negotiation and tool definitions
|
||||
- Properly retrieves GitHub/GitLab tokens from user secrets or environment variables
|
||||
|
||||
## Configuration
|
||||
|
||||
To configure the MCP server in your OpenHands configuration, add the following to your `config.toml` file:
|
||||
|
||||
```toml
|
||||
[mcp]
|
||||
# List of MCP SSE servers
|
||||
sse_servers = [
|
||||
{
|
||||
# The URL of the MCP server
|
||||
url = "http://localhost:12000/mcp",
|
||||
# Optional API key for authentication (not required for local development)
|
||||
api_key = ""
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The MCP server exposes the following tools:
|
||||
|
||||
### GitHub Pull Request Creation
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "callTool",
|
||||
"params": {
|
||||
"name": "create_github_pr",
|
||||
"arguments": {
|
||||
"repository": "owner/repo",
|
||||
"title": "Your PR title",
|
||||
"body": "Description of your changes",
|
||||
"head": "your-feature-branch",
|
||||
"base": "main",
|
||||
"draft": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GitLab Merge Request Creation
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "callTool",
|
||||
"params": {
|
||||
"name": "create_gitlab_mr",
|
||||
"arguments": {
|
||||
"project_id": "group/project",
|
||||
"title": "Your MR title",
|
||||
"description": "Description of your changes",
|
||||
"source_branch": "your-feature-branch",
|
||||
"target_branch": "main",
|
||||
"draft": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
The MCP server retrieves GitHub and GitLab tokens from the following sources, in order of precedence:
|
||||
|
||||
1. User secrets stored in the OpenHands settings store
|
||||
2. Environment variables (`GITHUB_TOKEN` and `GITLAB_TOKEN`)
|
||||
|
||||
If no token is found, the server will return an error.
|
||||
|
||||
## Microagent Integration
|
||||
|
||||
The GitHub and GitLab microagents have been updated to use the MCP server for creating pull requests and merge requests. This ensures that all PR/MR creation requests go through the standardized MCP interface, which provides better security and consistency.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The MCP server is implemented as a FastAPI router in `openhands/server/routes/mcp.py`. It handles the following MCP methods:
|
||||
|
||||
- `initialize`: Initialize the MCP session and negotiate capabilities
|
||||
- `shutdown`: Shut down the MCP session
|
||||
- `listTools`: List available tools
|
||||
- `callTool`: Call a specific tool with arguments
|
||||
|
||||
The server maintains session state for each client, including authentication tokens and service instances.
|
||||
@@ -49,4 +49,3 @@ The customization options you can set are:
|
||||
| `OPENHANDS_MACRO` | Variable | Customize default macro for invoking the resolver | `OPENHANDS_MACRO=@resolveit` |
|
||||
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](https://docs.all-hands.dev/modules/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
|
||||
| `TARGET_BRANCH` | Variable | Merge to branch other than `main` | `TARGET_BRANCH="dev"` |
|
||||
| `TARGET_RUNNER` | Variable | Target runner to execute the agent workflow (default ubuntu-latest) | `TARGET_RUNNER="custom-runner"` |
|
||||
|
||||
@@ -4,38 +4,6 @@
|
||||
OpenHands only supports Windows via WSL. Please be sure to run all commands inside your WSL terminal.
|
||||
:::
|
||||
|
||||
### Unable to access VS Code tab via local IP
|
||||
|
||||
**Description**
|
||||
|
||||
When accessing OpenHands through a non-localhost URL (such as a LAN IP address), the VS Code tab shows a "Forbidden" error, while other parts of the UI work fine.
|
||||
|
||||
**Resolution**
|
||||
|
||||
This happens because VS Code runs on a random high port that may not be exposed or accessible from other machines. To fix this:
|
||||
|
||||
1. Set a specific port for VS Code using the `SANDBOX_VSCODE_PORT` environment variable:
|
||||
```bash
|
||||
docker run -it --rm \
|
||||
-e SANDBOX_VSCODE_PORT=41234 \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:latest \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
-p 41234:41234 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:latest
|
||||
```
|
||||
|
||||
2. Make sure to expose the same port with `-p 41234:41234` in your Docker command.
|
||||
|
||||
3. Alternatively, you can set this in your `config.toml` file:
|
||||
```toml
|
||||
[sandbox]
|
||||
vscode_port = 41234
|
||||
```
|
||||
|
||||
### Launch docker client failed
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -215,11 +215,6 @@ const sidebars: SidebarsConfig = {
|
||||
label: 'Custom Sandbox',
|
||||
id: 'usage/how-to/custom-sandbox-guide',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'MCP',
|
||||
id: 'usage/mcp',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ function CustomFooter() {
|
||||
<footer className="custom-footer">
|
||||
<div className="footer-content">
|
||||
<div className="footer-icons">
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A" target="_blank" rel="noopener noreferrer">
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw" target="_blank" rel="noopener noreferrer">
|
||||
<FaSlack />
|
||||
</a>
|
||||
<a href="https://discord.gg/ESHStjSjD4" target="_blank" rel="noopener noreferrer">
|
||||
|
||||
@@ -47,7 +47,7 @@ export function HomepageHeader() {
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers" /></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License" /></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community" /></a>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community" /></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community" /></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits" /></a>
|
||||
<br/>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { screen, render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
// Mock modules before importing the component
|
||||
// Mock useParams before importing components
|
||||
vi.mock("react-router", async () => {
|
||||
const actual = await vi.importActual("react-router");
|
||||
return {
|
||||
@@ -11,11 +9,7 @@ vi.mock("react-router", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("#/context/conversation-context", () => ({
|
||||
useConversation: () => ({ conversationId: "test-conversation-id" }),
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
// Mock i18next
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual("react-i18next");
|
||||
return {
|
||||
@@ -29,56 +23,38 @@ vi.mock("react-i18next", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock redux
|
||||
const mockDispatch = vi.fn();
|
||||
let mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
};
|
||||
|
||||
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 { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
import { BrowserPanel } from "#/components/features/browser/browser";
|
||||
|
||||
describe("Browser", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset the mock state
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
};
|
||||
});
|
||||
|
||||
it("renders a message if no screenshotSrc is provided", () => {
|
||||
// Set the mock state for this test
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
renderWithProviders(<BrowserPanel />, {
|
||||
preloadedState: {
|
||||
browser: {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// i18n empty message key
|
||||
expect(screen.getByText("BROWSER$NO_PAGE_LOADED")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the url and a screenshot", () => {
|
||||
// Set the mock state for this test
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
renderWithProviders(<BrowserPanel />, {
|
||||
preloadedState: {
|
||||
browser: {
|
||||
url: "https://example.com",
|
||||
screenshotSrc:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText("https://example.com")).toBeInTheDocument();
|
||||
expect(screen.getByAltText("BROWSER$SCREENSHOT_ALT")).toBeInTheDocument();
|
||||
|
||||
@@ -34,6 +34,10 @@ describe("ConversationPanel", () => {
|
||||
}
|
||||
});
|
||||
|
||||
const { endSessionMock } = vi.hoisted(() => ({
|
||||
endSessionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-router")>()),
|
||||
@@ -42,6 +46,11 @@ describe("ConversationPanel", () => {
|
||||
useLocation: vi.fn(() => ({ pathname: "/conversation" })),
|
||||
useParams: vi.fn(() => ({ conversationId: "2" })),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-end-session", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("#/hooks/use-end-session")>()),
|
||||
useEndSession: vi.fn(() => endSessionMock),
|
||||
}));
|
||||
});
|
||||
|
||||
const mockConversations = [
|
||||
@@ -136,6 +145,47 @@ describe("ConversationPanel", () => {
|
||||
expect(cards).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should call endSession after deleting a conversation that is the current session", async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockData = [...mockConversations];
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
// Wait for React Query to update its cache
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
const ellipsisButton = within(cards[1]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
const deleteButton = screen.getByTestId("delete-button");
|
||||
|
||||
// Click the second delete button
|
||||
await user.click(deleteButton);
|
||||
|
||||
// Confirm the deletion
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update with a longer timeout
|
||||
await waitFor(() => {
|
||||
const updatedCards = screen.getAllByTestId("conversation-card");
|
||||
expect(updatedCards).toHaveLength(2);
|
||||
}, { timeout: 2000 });
|
||||
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should delete a conversation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockData = [
|
||||
|
||||
@@ -53,7 +53,6 @@ describe("HomeHeader", () => {
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// expect to be redirected to /conversations/:conversationId
|
||||
|
||||
@@ -171,7 +171,6 @@ describe("RepoConnector", () => {
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -95,7 +95,6 @@ describe("TaskCard", () => {
|
||||
[],
|
||||
undefined,
|
||||
MOCK_TASK_1,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,15 @@ describe("App", () => {
|
||||
{ Component: App, path: "/conversation/:conversationId" },
|
||||
]);
|
||||
|
||||
const { endSessionMock } = vi.hoisted(() => ({
|
||||
endSessionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("#/hooks/use-end-session", () => ({
|
||||
useEndSession: vi.fn(() => endSessionMock),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-terminal", () => ({
|
||||
useTerminal: vi.fn(),
|
||||
}));
|
||||
@@ -27,4 +35,44 @@ describe("App", () => {
|
||||
renderWithProviders(<RouteStub initialEntries={["/conversation/123"]} />);
|
||||
await screen.findByTestId("app-route");
|
||||
});
|
||||
|
||||
it("should call endSession if the user does not have permission to view conversation", async () => {
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue(null);
|
||||
renderWithProviders(<RouteStub initialEntries={["/conversation/9999"]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(errorToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call endSession if the user has permission", async () => {
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue({
|
||||
conversation_id: "9999",
|
||||
last_updated_at: "",
|
||||
created_at: "",
|
||||
title: "",
|
||||
selected_repository: "",
|
||||
status: "STOPPED",
|
||||
});
|
||||
const { rerender } = renderWithProviders(
|
||||
<RouteStub initialEntries={["/conversation/9999"]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).not.toHaveBeenCalled();
|
||||
expect(errorToastSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
rerender(<RouteStub initialEntries={["/conversation"]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).not.toHaveBeenCalled();
|
||||
expect(errorToastSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import {
|
||||
createAxiosNotFoundErrorObject,
|
||||
renderWithProviders,
|
||||
} from "test-utils";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
import i18n from "#/i18n";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
|
||||
describe("frontend/routes/_oh", () => {
|
||||
const RouteStub = createRoutesStub([{ Component: MainApp, path: "/" }]);
|
||||
@@ -176,32 +172,4 @@ describe("frontend/routes/_oh", () => {
|
||||
// expect(logoutCleanupSpy).toHaveBeenCalled();
|
||||
expect(localStorage.getItem("ghToken")).toBeNull();
|
||||
});
|
||||
|
||||
it("should render a you're in toast if it is a new user and in saas mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
"displaySuccessToast",
|
||||
);
|
||||
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "test-id",
|
||||
POSTHOG_CLIENT_KEY: "test-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
getSettingsSpy.mockRejectedValue(createAxiosNotFoundErrorObject());
|
||||
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(displaySuccessToastSpy).toHaveBeenCalledWith("BILLING$YOURE_IN");
|
||||
expect(displaySuccessToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,30 @@ import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Provider } from "react-redux";
|
||||
import { createAxiosNotFoundErrorObject, setupStore } from "test-utils";
|
||||
import { setupStore } from "test-utils";
|
||||
import { AxiosError } from "axios";
|
||||
import HomeScreen from "#/routes/home";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: MainApp,
|
||||
|
||||
@@ -516,47 +516,6 @@ describe("Form submission", () => {
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear advanced settings when saving basic settings", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
llm_api_key_set: true,
|
||||
confirmation_mode: true,
|
||||
});
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
renderLlmSettingsScreen();
|
||||
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
|
||||
const provider = screen.getByTestId("llm-provider-input");
|
||||
const model = screen.getByTestId("llm-model-input");
|
||||
|
||||
// select provider
|
||||
await userEvent.click(provider);
|
||||
const providerOption = screen.getByText("Anthropic");
|
||||
await userEvent.click(providerOption);
|
||||
|
||||
// select model
|
||||
await userEvent.click(model);
|
||||
const modelOption = screen.getByText("claude-3-5-sonnet-20241022");
|
||||
await userEvent.click(modelOption);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status toasts", () => {
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.8",
|
||||
"@microlink/react-json-view": "^1.26.1",
|
||||
@@ -82,7 +82,7 @@
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
@@ -9099,9 +9099,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
|
||||
"integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||
"integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -106,7 +106,7 @@
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
|
||||
import { GitUser, GitRepository, Branch } from "#/types/git";
|
||||
import { GitUser, GitRepository } from "#/types/git";
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
|
||||
class OpenHands {
|
||||
@@ -158,13 +158,12 @@ class OpenHands {
|
||||
imageUrls?: string[],
|
||||
replayJson?: string,
|
||||
suggested_task?: SuggestedTask,
|
||||
selected_branch?: string,
|
||||
): Promise<Conversation> {
|
||||
const body = {
|
||||
conversation_trigger,
|
||||
repository: selectedRepository,
|
||||
git_provider,
|
||||
selected_branch,
|
||||
selected_branch: undefined,
|
||||
initial_user_msg: initialUserMsg,
|
||||
image_urls: imageUrls,
|
||||
replay_json: replayJson,
|
||||
@@ -317,14 +316,6 @@ class OpenHands {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getRepositoryBranches(repository: string): Promise<Branch[]> {
|
||||
const { data } = await openHands.get<Branch[]>(
|
||||
`/api/user/repository/branches?repository=${encodeURIComponent(repository)}`,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenHands;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2.25C6.624 2.25 2.25 6.624 2.25 12C2.25 16.376 5.115 20.073 9.057 21.428C9.563 21.514 9.747 21.211 9.747 20.95C9.747 20.713 9.738 19.991 9.738 19.153C7 19.713 6.4 18.45 6.4 18.45C5.952 17.387 5.328 17.084 5.328 17.084C4.476 16.487 5.387 16.487 5.387 16.487C6.328 16.546 6.85 17.481 6.85 17.481C7.675 18.862 9.057 18.487 9.776 18.226C9.867 17.603 10.133 17.179 10.419 16.94C8.287 16.713 6.05 15.85 6.05 11.9C6.05 10.837 6.4 9.975 6.859 9.3C6.759 9.05 6.45 8.038 6.95 6.65C6.95 6.65 7.734 6.4 9.738 7.775C10.483 7.534 11.25 7.413 12.017 7.413C12.784 7.413 13.55 7.534 14.296 7.775C16.3 6.4 17.084 6.65 17.084 6.65C17.584 8.038 17.275 9.05 17.175 9.3C17.634 9.975 17.984 10.837 17.984 11.9C17.984 15.85 15.747 16.7 13.615 16.94C13.975 17.237 14.296 17.813 14.296 18.7C14.296 19.975 14.287 20.6 14.287 20.95C14.287 21.211 14.471 21.514 14.977 21.428C18.919 20.073 21.784 16.376 21.784 12C21.784 6.624 17.376 2.25 12 2.25Z" fill="currentColor"/>
|
||||
<path
|
||||
d="M15.359 21V17.319C15.3974 16.8654 15.3314 16.4095 15.1651 15.9814C14.9989 15.5534 14.7363 15.1631 14.3949 14.8364C17.6154 14.5035 21 13.3716 21 8.17826C20.9997 6.85027 20.4489 5.57321 19.4615 4.61139C19.9291 3.44954 19.896 2.16532 19.3692 1.02548C19.3692 1.02548 18.159 0.692576 15.359 2.43321C13.0082 1.84237 10.5302 1.84237 8.17949 2.43321C5.37949 0.692576 4.16923 1.02548 4.16923 1.02548C3.64244 2.16532 3.60938 3.44954 4.07692 4.61139C3.08218 5.58034 2.53079 6.86895 2.53846 8.2068C2.53846 13.3621 5.92308 14.494 9.14359 14.865C8.80615 15.1883 8.54591 15.574 8.3798 15.9968C8.2137 16.4196 8.14544 16.8701 8.17949 17.319V21M8.17949 18.1465C3.05128 19.5732 3.05128 15.7686 1 15.293L8.17949 18.1465Z"
|
||||
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 896 B |
@@ -1,8 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="97 99 186 182">
|
||||
<defs>
|
||||
<style>.cls-1{fill:currentColor;}</style>
|
||||
</defs>
|
||||
<g id="LOGO">
|
||||
<path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/>
|
||||
</g>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 21L16.5 8H5.5L11 21Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 8L3.5 15.5L11 21L18.5 15.5L21 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 8L5.5 8L8.25 1" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 8L16.5 8L13.75 1" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 534 B |
@@ -1,26 +1,12 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { BrowserSnapshot } from "./browser-snapshot";
|
||||
import { EmptyBrowserMessage } from "./empty-browser-message";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
setUrl,
|
||||
setScreenshotSrc,
|
||||
} from "#/state/browser-slice";
|
||||
|
||||
export function BrowserPanel() {
|
||||
const { url, screenshotSrc } = useSelector(
|
||||
(state: RootState) => state.browser,
|
||||
);
|
||||
const { conversationId } = useConversation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setUrl(browserInitialState.url));
|
||||
dispatch(setScreenshotSrc(browserInitialState.screenshotSrc));
|
||||
}, [conversationId]);
|
||||
|
||||
const imgSrc =
|
||||
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { NavLink, useParams, useNavigate } from "react-router";
|
||||
import { NavLink, useParams } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { ConversationCard } from "./conversation-card";
|
||||
@@ -8,6 +8,7 @@ import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation"
|
||||
import { ConfirmDeleteModal } from "./confirm-delete-modal";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { ExitConversationModal } from "./exit-conversation-modal";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
|
||||
@@ -17,9 +18,9 @@ interface ConversationPanelProps {
|
||||
|
||||
export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const { conversationId: currentConversationId } = useParams();
|
||||
const { conversationId: cid } = useParams();
|
||||
const endSession = useEndSession();
|
||||
const ref = useClickOutsideElement<HTMLDivElement>(onClose);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
|
||||
React.useState(false);
|
||||
@@ -47,8 +48,8 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
{ conversationId: selectedConversationId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (selectedConversationId === currentConversationId) {
|
||||
navigate("/");
|
||||
if (cid === selectedConversationId) {
|
||||
endSession();
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -128,6 +129,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
{confirmExitConversationModalVisible && (
|
||||
<ExitConversationModal
|
||||
onConfirm={() => {
|
||||
endSession();
|
||||
onClose();
|
||||
}}
|
||||
onClose={() => setConfirmExitConversationModalVisible(false)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { RepositorySelectionForm } from "./repo-selection-form";
|
||||
|
||||
// Create mock functions
|
||||
const mockUseUserRepositories = vi.fn();
|
||||
const mockUseRepositoryBranches = vi.fn();
|
||||
const mockUseCreateConversation = vi.fn();
|
||||
const mockUseIsCreatingConversation = vi.fn();
|
||||
const mockUseTranslation = vi.fn();
|
||||
@@ -17,12 +16,6 @@ mockUseUserRepositories.mockReturnValue({
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseRepositoryBranches.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseCreateConversation.mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
@@ -54,10 +47,6 @@ vi.mock("#/hooks/query/use-user-repositories", () => ({
|
||||
useUserRepositories: () => mockUseUserRepositories(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-repository-branches", () => ({
|
||||
useRepositoryBranches: () => mockUseRepositoryBranches(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/mutation/use-create-conversation", () => ({
|
||||
useCreateConversation: () => mockUseCreateConversation(),
|
||||
}));
|
||||
|
||||
@@ -1,42 +1,79 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { useRepositoryBranches } from "#/hooks/query/use-repository-branches";
|
||||
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
|
||||
import { Branch, GitRepository } from "#/types/git";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import {
|
||||
RepositoryDropdown,
|
||||
RepositoryLoadingState,
|
||||
RepositoryErrorState,
|
||||
BranchDropdown,
|
||||
BranchLoadingState,
|
||||
BranchErrorState,
|
||||
} from "./repository-selection";
|
||||
import { SettingsDropdownInput } from "../settings/settings-dropdown-input";
|
||||
|
||||
interface RepositorySelectionFormProps {
|
||||
onRepoSelection: (repoTitle: string | null) => void;
|
||||
}
|
||||
|
||||
// Loading state component
|
||||
function RepositoryLoadingState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-loading"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">{t("HOME$LOADING_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state component
|
||||
function RepositoryErrorState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-error"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
|
||||
>
|
||||
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Repository dropdown component
|
||||
interface RepositoryDropdownProps {
|
||||
items: { key: React.Key; label: string }[];
|
||||
onSelectionChange: (key: React.Key | null) => void;
|
||||
onInputChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function RepositoryDropdown({
|
||||
items,
|
||||
onSelectionChange,
|
||||
onInputChange,
|
||||
}: RepositoryDropdownProps) {
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="repo-dropdown"
|
||||
name="repo-dropdown"
|
||||
placeholder="Select a repo"
|
||||
items={items}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={onSelectionChange}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RepositorySelectionForm({
|
||||
onRepoSelection,
|
||||
}: RepositorySelectionFormProps) {
|
||||
const [selectedRepository, setSelectedRepository] =
|
||||
React.useState<GitRepository | null>(null);
|
||||
const [selectedBranch, setSelectedBranch] = React.useState<Branch | null>(
|
||||
null,
|
||||
);
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
isError: isRepositoriesError,
|
||||
} = useUserRepositories();
|
||||
const {
|
||||
data: branches,
|
||||
isLoading: isLoadingBranches,
|
||||
isError: isBranchesError,
|
||||
} = useRepositoryBranches(selectedRepository?.full_name || null);
|
||||
const {
|
||||
mutate: createConversation,
|
||||
isPending,
|
||||
@@ -45,27 +82,6 @@ export function RepositorySelectionForm({
|
||||
const isCreatingConversationElsewhere = useIsCreatingConversation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Auto-select main or master branch if it exists
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
branches &&
|
||||
branches.length > 0 &&
|
||||
!selectedBranch &&
|
||||
!isLoadingBranches
|
||||
) {
|
||||
// Look for main or master branch
|
||||
const mainBranch = branches.find((branch) => branch.name === "main");
|
||||
const masterBranch = branches.find((branch) => branch.name === "master");
|
||||
|
||||
// Select main if it exists, otherwise select master if it exists
|
||||
if (mainBranch) {
|
||||
setSelectedBranch(mainBranch);
|
||||
} else if (masterBranch) {
|
||||
setSelectedBranch(masterBranch);
|
||||
}
|
||||
}
|
||||
}, [branches, selectedBranch, isLoadingBranches]);
|
||||
|
||||
// We check for isSuccess because the app might require time to render
|
||||
// into the new conversation screen after the conversation is created.
|
||||
const isCreatingConversation =
|
||||
@@ -76,11 +92,6 @@ export function RepositorySelectionForm({
|
||||
label: repo.full_name,
|
||||
}));
|
||||
|
||||
const branchesItems = branches?.map((branch) => ({
|
||||
key: branch.name,
|
||||
label: branch.name,
|
||||
}));
|
||||
|
||||
const handleRepoSelection = (key: React.Key | null) => {
|
||||
const selectedRepo = repositories?.find(
|
||||
(repo) => repo.id.toString() === key,
|
||||
@@ -88,28 +99,15 @@ export function RepositorySelectionForm({
|
||||
|
||||
if (selectedRepo) onRepoSelection(selectedRepo.full_name);
|
||||
setSelectedRepository(selectedRepo || null);
|
||||
setSelectedBranch(null); // Reset branch selection when repo changes
|
||||
};
|
||||
|
||||
const handleBranchSelection = (key: React.Key | null) => {
|
||||
const selectedBranchObj = branches?.find((branch) => branch.name === key);
|
||||
setSelectedBranch(selectedBranchObj || null);
|
||||
};
|
||||
|
||||
const handleRepoInputChange = (value: string) => {
|
||||
const handleInputChange = (value: string) => {
|
||||
if (value === "") {
|
||||
setSelectedRepository(null);
|
||||
setSelectedBranch(null);
|
||||
onRepoSelection(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBranchInputChange = (value: string) => {
|
||||
if (value === "") {
|
||||
setSelectedBranch(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Render the appropriate UI based on the loading/error state
|
||||
const renderRepositorySelector = () => {
|
||||
if (isLoadingRepositories) {
|
||||
@@ -124,49 +122,15 @@ export function RepositorySelectionForm({
|
||||
<RepositoryDropdown
|
||||
items={repositoriesItems || []}
|
||||
onSelectionChange={handleRepoSelection}
|
||||
onInputChange={handleRepoInputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Render the appropriate UI for branch selector based on the loading/error state
|
||||
const renderBranchSelector = () => {
|
||||
if (!selectedRepository) {
|
||||
return (
|
||||
<BranchDropdown
|
||||
items={[]}
|
||||
onSelectionChange={() => {}}
|
||||
onInputChange={() => {}}
|
||||
isDisabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoadingBranches) {
|
||||
return <BranchLoadingState />;
|
||||
}
|
||||
|
||||
if (isBranchesError) {
|
||||
return <BranchErrorState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BranchDropdown
|
||||
items={branchesItems || []}
|
||||
onSelectionChange={handleBranchSelection}
|
||||
onInputChange={handleBranchInputChange}
|
||||
isDisabled={false}
|
||||
selectedKey={selectedBranch?.name}
|
||||
onInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<>
|
||||
{renderRepositorySelector()}
|
||||
|
||||
{renderBranchSelector()}
|
||||
|
||||
<BrandButton
|
||||
testId="repo-launch-button"
|
||||
variant="primary"
|
||||
@@ -181,13 +145,12 @@ export function RepositorySelectionForm({
|
||||
createConversation({
|
||||
selectedRepository,
|
||||
conversation_trigger: "gui",
|
||||
selected_branch: selectedBranch?.name,
|
||||
})
|
||||
}
|
||||
>
|
||||
{!isCreatingConversation && "Launch"}
|
||||
{isCreatingConversation && t("HOME$LOADING")}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from "react";
|
||||
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
|
||||
|
||||
export interface BranchDropdownProps {
|
||||
items: { key: React.Key; label: string }[];
|
||||
onSelectionChange: (key: React.Key | null) => void;
|
||||
onInputChange: (value: string) => void;
|
||||
isDisabled: boolean;
|
||||
selectedKey?: string;
|
||||
}
|
||||
|
||||
export function BranchDropdown({
|
||||
items,
|
||||
onSelectionChange,
|
||||
onInputChange,
|
||||
isDisabled,
|
||||
selectedKey,
|
||||
}: BranchDropdownProps) {
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="branch-dropdown"
|
||||
name="branch-dropdown"
|
||||
placeholder="Select a branch"
|
||||
items={items}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={onSelectionChange}
|
||||
onInputChange={onInputChange}
|
||||
isDisabled={isDisabled}
|
||||
selectedKey={selectedKey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function BranchErrorState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="branch-dropdown-error"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
|
||||
>
|
||||
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_BRANCHES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
|
||||
export function BranchLoadingState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="branch-dropdown-loading"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">{t("HOME$LOADING_BRANCHES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export { RepositoryDropdown } from "#/components/features/home/repository-selection/repository-dropdown";
|
||||
export { RepositoryLoadingState } from "#/components/features/home/repository-selection/repository-loading-state";
|
||||
export { RepositoryErrorState } from "#/components/features/home/repository-selection/repository-error-state";
|
||||
export { BranchDropdown } from "#/components/features/home/repository-selection/branch-dropdown";
|
||||
export { BranchLoadingState } from "#/components/features/home/repository-selection/branch-loading-state";
|
||||
export { BranchErrorState } from "#/components/features/home/repository-selection/branch-error-state";
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from "react";
|
||||
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
|
||||
|
||||
export interface RepositoryDropdownProps {
|
||||
items: { key: React.Key; label: string }[];
|
||||
onSelectionChange: (key: React.Key | null) => void;
|
||||
onInputChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function RepositoryDropdown({
|
||||
items,
|
||||
onSelectionChange,
|
||||
onInputChange,
|
||||
}: RepositoryDropdownProps) {
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="repo-dropdown"
|
||||
name="repo-dropdown"
|
||||
placeholder="Select a repo"
|
||||
items={items}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={onSelectionChange}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function RepositoryErrorState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-error"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
|
||||
>
|
||||
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
|
||||
export function RepositoryLoadingState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-loading"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">{t("HOME$LOADING_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ interface SettingsDropdownInputProps {
|
||||
showOptionalTag?: boolean;
|
||||
isDisabled?: boolean;
|
||||
defaultSelectedKey?: string;
|
||||
selectedKey?: string;
|
||||
isClearable?: boolean;
|
||||
onSelectionChange?: (key: React.Key | null) => void;
|
||||
onInputChange?: (value: string) => void;
|
||||
@@ -29,7 +28,6 @@ export function SettingsDropdownInput({
|
||||
showOptionalTag,
|
||||
isDisabled,
|
||||
defaultSelectedKey,
|
||||
selectedKey,
|
||||
isClearable,
|
||||
onSelectionChange,
|
||||
onInputChange,
|
||||
@@ -48,7 +46,6 @@ export function SettingsDropdownInput({
|
||||
name={name}
|
||||
defaultItems={items}
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
selectedKey={selectedKey}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onInputChange={onInputChange}
|
||||
isClearable={isClearable}
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import React from "react";
|
||||
import { FaListUl } from "react-icons/fa";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { useLocation } from "react-router";
|
||||
import { NavLink, useLocation } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGitUser } from "#/hooks/query/use-git-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
|
||||
import { DocsButton } from "#/components/shared/buttons/docs-button";
|
||||
import { NewProjectButton } from "#/components/shared/buttons/new-project-button";
|
||||
import { ExitProjectButton } from "#/components/shared/buttons/exit-project-button";
|
||||
import { SettingsButton } from "#/components/shared/buttons/settings-button";
|
||||
import { ConversationPanelButton } from "#/components/shared/buttons/conversation-panel-button";
|
||||
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { ConversationPanel } from "../conversation-panel/conversation-panel";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
|
||||
import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper";
|
||||
import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function Sidebar() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
const user = useGitUser();
|
||||
const { data: config } = useConfig();
|
||||
const {
|
||||
@@ -62,6 +73,11 @@ export function Sidebar() {
|
||||
location.pathname,
|
||||
]);
|
||||
|
||||
const handleEndSession = () => {
|
||||
dispatch(setCurrentAgentState(AgentState.LOADING));
|
||||
endSession();
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
posthog.reset();
|
||||
@@ -73,18 +89,34 @@ export function Sidebar() {
|
||||
<nav className="flex flex-row md:flex-col items-center justify-between w-full h-auto md:w-auto md:h-full">
|
||||
<div className="flex flex-row md:flex-col items-center gap-[26px]">
|
||||
<div className="flex items-center justify-center">
|
||||
<AllHandsLogoButton />
|
||||
<AllHandsLogoButton onClick={handleEndSession} />
|
||||
</div>
|
||||
<NewProjectButton />
|
||||
<ConversationPanelButton
|
||||
isOpen={conversationPanelIsOpen}
|
||||
<ExitProjectButton onClick={handleEndSession} />
|
||||
<TooltipButton
|
||||
testId="toggle-conversation-panel"
|
||||
tooltip={t(I18nKey.SIDEBAR$CONVERSATIONS)}
|
||||
ariaLabel={t(I18nKey.SIDEBAR$CONVERSATIONS)}
|
||||
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
|
||||
/>
|
||||
>
|
||||
<FaListUl
|
||||
size={22}
|
||||
className={cn(
|
||||
conversationPanelIsOpen ? "text-white" : "text-[#9099AC]",
|
||||
)}
|
||||
/>
|
||||
</TooltipButton>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row md:flex-col md:items-center gap-[26px] md:mb-4">
|
||||
<DocsButton />
|
||||
<SettingsButton />
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className={({ isActive }) =>
|
||||
`${isActive ? "text-white" : "text-[#9099AC]"} mt-0.5 md:mt-0`
|
||||
}
|
||||
>
|
||||
<SettingsButton />
|
||||
</NavLink>
|
||||
<UserActions
|
||||
user={
|
||||
user.data ? { avatar_url: user.data.avatar_url } : undefined
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { getRandomTip } from "#/utils/tips";
|
||||
|
||||
export function RandomTip() {
|
||||
const { t } = useTranslation();
|
||||
const [randomTip, setRandomTip] = React.useState(getRandomTip());
|
||||
|
||||
// Update the random tip when the component mounts
|
||||
React.useEffect(() => {
|
||||
setRandomTip(getRandomTip());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<p>
|
||||
<h4 className="font-bold">{t(I18nKey.TIPS$PROTIP)}:</h4>
|
||||
{t(randomTip.key)}
|
||||
{randomTip.link && (
|
||||
<>
|
||||
{" "}
|
||||
<a
|
||||
href={randomTip.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{t(I18nKey.TIPS$LEARN_MORE)}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { LoadingSpinner } from "../shared/loading-spinner";
|
||||
|
||||
// Lazy load all tab components
|
||||
const EditorTab = lazy(() => import("#/routes/changes-tab"));
|
||||
const BrowserTab = lazy(() => import("#/routes/browser-tab"));
|
||||
const JupyterTab = lazy(() => import("#/routes/jupyter-tab"));
|
||||
const ServedTab = lazy(() => import("#/routes/served-tab"));
|
||||
const TerminalTab = lazy(() => import("#/routes/terminal-tab"));
|
||||
const VSCodeTab = lazy(() => import("#/routes/vscode-tab"));
|
||||
|
||||
interface TabContentProps {
|
||||
conversationPath: string;
|
||||
}
|
||||
|
||||
export function TabContent({ conversationPath }: TabContentProps) {
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname;
|
||||
|
||||
// Determine which tab is active based on the current path
|
||||
const isEditorActive = currentPath === conversationPath;
|
||||
const isBrowserActive = currentPath === `${conversationPath}/browser`;
|
||||
const isJupyterActive = currentPath === `${conversationPath}/jupyter`;
|
||||
const isServedActive = currentPath === `${conversationPath}/served`;
|
||||
const isTerminalActive = currentPath === `${conversationPath}/terminal`;
|
||||
const isVSCodeActive = currentPath === `${conversationPath}/vscode`;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative">
|
||||
{/* Each tab content is always loaded but only visible when active */}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<LoadingSpinner size="large" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 ${isEditorActive ? "block" : "hidden"}`}
|
||||
>
|
||||
<EditorTab />
|
||||
</div>
|
||||
<div
|
||||
className={`absolute inset-0 ${isBrowserActive ? "block" : "hidden"}`}
|
||||
>
|
||||
<BrowserTab />
|
||||
</div>
|
||||
<div
|
||||
className={`absolute inset-0 ${isJupyterActive ? "block" : "hidden"}`}
|
||||
>
|
||||
<JupyterTab />
|
||||
</div>
|
||||
<div
|
||||
className={`absolute inset-0 ${isServedActive ? "block" : "hidden"}`}
|
||||
>
|
||||
<ServedTab />
|
||||
</div>
|
||||
<div
|
||||
className={`absolute inset-0 ${isTerminalActive ? "block" : "hidden"}`}
|
||||
>
|
||||
<TerminalTab />
|
||||
</div>
|
||||
<div
|
||||
className={`absolute inset-0 ${isVSCodeActive ? "block" : "hidden"}`}
|
||||
>
|
||||
<VSCodeTab />
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
export function AllHandsLogoButton() {
|
||||
const { t } = useTranslation();
|
||||
interface AllHandsLogoButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<TooltipButton
|
||||
tooltip={t(I18nKey.BRANDING$ALL_HANDS_AI)}
|
||||
ariaLabel={t(I18nKey.BRANDING$ALL_HANDS_LOGO)}
|
||||
navLinkTo="/"
|
||||
onClick={onClick}
|
||||
>
|
||||
<AllHandsLogo width={34} height={34} />
|
||||
</TooltipButton>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from "react";
|
||||
import { FaListUl } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ConversationPanelButtonProps {
|
||||
isOpen: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ConversationPanelButton({
|
||||
isOpen,
|
||||
onClick,
|
||||
}: ConversationPanelButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipButton
|
||||
testId="toggle-conversation-panel"
|
||||
tooltip={t(I18nKey.SIDEBAR$CONVERSATIONS)}
|
||||
ariaLabel={t(I18nKey.SIDEBAR$CONVERSATIONS)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<FaListUl
|
||||
size={22}
|
||||
className={cn(isOpen ? "text-white" : "text-[#9099AC]")}
|
||||
/>
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
+7
-3
@@ -3,17 +3,21 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import PlusIcon from "#/icons/plus.svg?react";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
export function NewProjectButton() {
|
||||
interface ExitProjectButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const startNewProject = t(I18nKey.CONVERSATION$START_NEW);
|
||||
return (
|
||||
<TooltipButton
|
||||
tooltip={startNewProject}
|
||||
ariaLabel={startNewProject}
|
||||
navLinkTo="/"
|
||||
onClick={onClick}
|
||||
testId="new-project-button"
|
||||
>
|
||||
<PlusIcon width={28} height={28} />
|
||||
<PlusIcon width={28} height={28} className="text-[#9099AC]" />
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export function SettingsButton({ onClick }: SettingsButtonProps) {
|
||||
tooltip={t(I18nKey.SETTINGS$TITLE)}
|
||||
ariaLabel={t(I18nKey.SETTINGS$TITLE)}
|
||||
onClick={onClick}
|
||||
navLinkTo="/settings"
|
||||
>
|
||||
<SettingsIcon width={28} height={28} />
|
||||
</TooltipButton>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Tooltip } from "@heroui/react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { NavLink } from "react-router";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
export interface TooltipButtonProps {
|
||||
interface TooltipButtonProps {
|
||||
children: ReactNode;
|
||||
tooltip: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
navLinkTo?: string;
|
||||
ariaLabel: string;
|
||||
testId?: string;
|
||||
className?: React.HTMLAttributes<HTMLButtonElement>["className"];
|
||||
@@ -19,66 +17,35 @@ export function TooltipButton({
|
||||
tooltip,
|
||||
onClick,
|
||||
href,
|
||||
navLinkTo,
|
||||
ariaLabel,
|
||||
testId,
|
||||
className,
|
||||
}: TooltipButtonProps) {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const buttonContent = (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
onClick={handleClick}
|
||||
onClick={onClick}
|
||||
className={cn("hover:opacity-80", className)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
let content;
|
||||
|
||||
if (navLinkTo) {
|
||||
content = (
|
||||
<NavLink
|
||||
to={navLinkTo}
|
||||
onClick={handleClick}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"hover:opacity-80",
|
||||
isActive ? "text-white" : "text-[#9099AC]",
|
||||
className,
|
||||
)
|
||||
}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
} else if (href) {
|
||||
content = (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className={cn("hover:opacity-80", className)}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
content = buttonContent;
|
||||
}
|
||||
const content = href ? (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className={cn("hover:opacity-80", className)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
buttonContent
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip} closeDelay={100} placement="right">
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { DangerModal } from "./confirmation-modals/danger-modal";
|
||||
import { ModalBackdrop } from "./modal-backdrop";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ExitProjectConfirmationModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ExitProjectConfirmationModal({
|
||||
onClose,
|
||||
}: ExitProjectConfirmationModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
|
||||
const handleEndSession = () => {
|
||||
onClose();
|
||||
dispatch(setCurrentAgentState(AgentState.LOADING));
|
||||
endSession();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
<DangerModal
|
||||
title={t(I18nKey.EXIT_PROJECT$CONFIRM)}
|
||||
description={t(I18nKey.EXIT_PROJECT$WARNING)}
|
||||
buttons={{
|
||||
danger: {
|
||||
text: t(I18nKey.EXIT_PROJECT$TITLE),
|
||||
onClick: handleEndSession,
|
||||
},
|
||||
cancel: {
|
||||
text: t(I18nKey.BUTTON$CANCEL),
|
||||
onClick: onClose,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import { organizeModelsAndProviders } from "#/utils/organize-models-and-providers";
|
||||
import { DangerModal } from "../confirmation-modals/danger-modal";
|
||||
import { extractSettings } from "#/utils/settings-utils";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { Settings } from "#/types/settings";
|
||||
@@ -23,6 +24,7 @@ interface SettingsFormProps {
|
||||
|
||||
export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
const endSession = useEndSession();
|
||||
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
@@ -32,12 +34,19 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
const [confirmEndSessionModalOpen, setConfirmEndSessionModalOpen] =
|
||||
React.useState(false);
|
||||
|
||||
const resetOngoingSession = () => {
|
||||
if (location.pathname.startsWith("/conversations/")) {
|
||||
endSession();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmission = async (formData: FormData) => {
|
||||
const newSettings = extractSettings(formData);
|
||||
|
||||
await saveUserSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
resetOngoingSession();
|
||||
|
||||
posthog.capture("settings_saved", {
|
||||
LLM_MODEL: newSettings.LLM_MODEL,
|
||||
|
||||
@@ -24,7 +24,7 @@ export const useCreateConversation = () => {
|
||||
conversation_trigger: ConversationTrigger;
|
||||
q?: string;
|
||||
selectedRepository?: GitRepository | null;
|
||||
selected_branch?: string;
|
||||
|
||||
suggested_task?: SuggestedTask;
|
||||
}) => {
|
||||
if (variables.q) dispatch(setInitialPrompt(variables.q));
|
||||
@@ -41,7 +41,6 @@ export const useCreateConversation = () => {
|
||||
files,
|
||||
replayJson || undefined,
|
||||
variables.suggested_task || undefined,
|
||||
variables.selected_branch,
|
||||
);
|
||||
},
|
||||
onSuccess: async ({ conversation_id: conversationId }, { q }) => {
|
||||
|
||||
@@ -20,8 +20,6 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: settings.user_consents_to_analytics,
|
||||
enable_proactive_conversation_starters:
|
||||
settings.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
|
||||
};
|
||||
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { Branch } from "#/types/git";
|
||||
|
||||
export const useRepositoryBranches = (repository: string | null) =>
|
||||
useQuery<Branch[]>({
|
||||
queryKey: ["repository", repository, "branches"],
|
||||
queryFn: async () => {
|
||||
if (!repository) return [];
|
||||
return OpenHands.getRepositoryBranches(repository);
|
||||
},
|
||||
enabled: !!repository,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
@@ -22,8 +22,6 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
|
||||
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
|
||||
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
|
||||
apiSettings.enable_proactive_conversation_starters,
|
||||
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
|
||||
IS_NEW_USER: false,
|
||||
};
|
||||
|
||||
@@ -1,43 +1,19 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
|
||||
// Define the return type for the VS Code URL query
|
||||
interface VSCodeUrlResult {
|
||||
url: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const useVSCodeUrl = () => {
|
||||
const { t } = useTranslation();
|
||||
export const useVSCodeUrl = (config: { enabled: boolean }) => {
|
||||
const { conversationId } = useConversation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
|
||||
|
||||
return useQuery<VSCodeUrlResult>({
|
||||
const data = useQuery({
|
||||
queryKey: ["vscode_url", conversationId],
|
||||
queryFn: async () => {
|
||||
queryFn: () => {
|
||||
if (!conversationId) throw new Error("No conversation ID");
|
||||
const data = await OpenHands.getVSCodeUrl(conversationId);
|
||||
if (data.vscode_url) {
|
||||
return {
|
||||
url: transformVSCodeUrl(data.vscode_url),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
url: null,
|
||||
error: t(I18nKey.VSCODE$URL_NOT_AVAILABLE),
|
||||
};
|
||||
return OpenHands.getVSCodeUrl(conversationId);
|
||||
},
|
||||
enabled: !!conversationId && !isRuntimeInactive,
|
||||
enabled: !!conversationId && config.enabled,
|
||||
refetchOnMount: true,
|
||||
retry: 3,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
setScreenshotSrc,
|
||||
setUrl,
|
||||
} from "#/state/browser-slice";
|
||||
import { clearSelectedRepository } from "#/state/initial-query-slice";
|
||||
|
||||
export const useEndSession = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
/**
|
||||
* End the current session by clearing the token and redirecting to the home page.
|
||||
*/
|
||||
const endSession = () => {
|
||||
dispatch(clearSelectedRepository());
|
||||
|
||||
// Reset browser state to initial values
|
||||
dispatch(setUrl(browserInitialState.url));
|
||||
dispatch(setScreenshotSrc(browserInitialState.screenshotSrc));
|
||||
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return endSession;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { addErrorMessage } from "#/state/chat-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { useEndSession } from "./use-end-session";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
interface ServerError {
|
||||
@@ -20,6 +21,7 @@ const isErrorObservation = (data: object): data is ErrorObservation =>
|
||||
|
||||
export const useHandleWSEvents = () => {
|
||||
const { events, send } = useWsClient();
|
||||
const endSession = useEndSession();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -31,6 +33,7 @@ export const useHandleWSEvents = () => {
|
||||
if (isServerError(event)) {
|
||||
if (event.error_code === 401) {
|
||||
displayErrorToast("Session expired.");
|
||||
endSession();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ export enum I18nKey {
|
||||
HOME$LOADING = "HOME$LOADING",
|
||||
HOME$LOADING_REPOSITORIES = "HOME$LOADING_REPOSITORIES",
|
||||
HOME$FAILED_TO_LOAD_REPOSITORIES = "HOME$FAILED_TO_LOAD_REPOSITORIES",
|
||||
HOME$LOADING_BRANCHES = "HOME$LOADING_BRANCHES",
|
||||
HOME$FAILED_TO_LOAD_BRANCHES = "HOME$FAILED_TO_LOAD_BRANCHES",
|
||||
HOME$OPEN_ISSUE = "HOME$OPEN_ISSUE",
|
||||
HOME$FIX_FAILING_CHECKS = "HOME$FIX_FAILING_CHECKS",
|
||||
HOME$RESOLVE_MERGE_CONFLICTS = "HOME$RESOLVE_MERGE_CONFLICTS",
|
||||
@@ -70,7 +68,6 @@ export enum I18nKey {
|
||||
SETTINGS$LLM_SETTINGS = "SETTINGS$LLM_SETTINGS",
|
||||
SETTINGS$GIT_SETTINGS = "SETTINGS$GIT_SETTINGS",
|
||||
SETTINGS$SOUND_NOTIFICATIONS = "SETTINGS$SOUND_NOTIFICATIONS",
|
||||
SETTINGS$PROACTIVE_CONVERSATION_STARTERS = "SETTINGS$PROACTIVE_CONVERSATION_STARTERS",
|
||||
SETTINGS$CUSTOM_MODEL = "SETTINGS$CUSTOM_MODEL",
|
||||
GITHUB$CODE_NOT_IN_GITHUB = "GITHUB$CODE_NOT_IN_GITHUB",
|
||||
GITHUB$START_FROM_SCRATCH = "GITHUB$START_FROM_SCRATCH",
|
||||
@@ -313,7 +310,6 @@ export enum I18nKey {
|
||||
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
|
||||
STATUS$CONTAINER_STARTED = "STATUS$CONTAINER_STARTED",
|
||||
STATUS$SETTING_UP_WORKSPACE = "STATUS$SETTING_UP_WORKSPACE",
|
||||
STATUS$SETTING_UP_GIT_HOOKS = "STATUS$SETTING_UP_GIT_HOOKS",
|
||||
ACCOUNT_SETTINGS_MODAL$DISCONNECT = "ACCOUNT_SETTINGS_MODAL$DISCONNECT",
|
||||
ACCOUNT_SETTINGS_MODAL$SAVE = "ACCOUNT_SETTINGS_MODAL$SAVE",
|
||||
ACCOUNT_SETTINGS_MODAL$CLOSE = "ACCOUNT_SETTINGS_MODAL$CLOSE",
|
||||
@@ -478,16 +474,4 @@ export enum I18nKey {
|
||||
TOS$ACCEPT_TERMS_DESCRIPTION = "TOS$ACCEPT_TERMS_DESCRIPTION",
|
||||
TOS$CONTINUE = "TOS$CONTINUE",
|
||||
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
|
||||
TIPS$CUSTOMIZE_MICROAGENT = "TIPS$CUSTOMIZE_MICROAGENT",
|
||||
TIPS$SETUP_SCRIPT = "TIPS$SETUP_SCRIPT",
|
||||
TIPS$VSCODE_INSTANCE = "TIPS$VSCODE_INSTANCE",
|
||||
TIPS$SAVE_WORK = "TIPS$SAVE_WORK",
|
||||
TIPS$SPECIFY_FILES = "TIPS$SPECIFY_FILES",
|
||||
TIPS$HEADLESS_MODE = "TIPS$HEADLESS_MODE",
|
||||
TIPS$CLI_MODE = "TIPS$CLI_MODE",
|
||||
TIPS$GITHUB_HOOK = "TIPS$GITHUB_HOOK",
|
||||
TIPS$BLOG_SIGNUP = "TIPS$BLOG_SIGNUP",
|
||||
TIPS$API_USAGE = "TIPS$API_USAGE",
|
||||
TIPS$LEARN_MORE = "TIPS$LEARN_MORE",
|
||||
TIPS$PROTIP = "TIPS$PROTIP",
|
||||
}
|
||||
|
||||
@@ -119,36 +119,6 @@
|
||||
"tr": "Depolar yüklenemedi",
|
||||
"de": "Fehler beim Laden der Repositories"
|
||||
},
|
||||
"HOME$LOADING_BRANCHES": {
|
||||
"en": "Loading branches...",
|
||||
"ja": "ブランチを読み込み中...",
|
||||
"zh-CN": "正在加载分支...",
|
||||
"zh-TW": "正在加載分支...",
|
||||
"ko-KR": "브랜치 로딩 중...",
|
||||
"no": "Laster inn branches...",
|
||||
"it": "Caricamento dei branch...",
|
||||
"pt": "Carregando branches...",
|
||||
"es": "Cargando ramas...",
|
||||
"ar": "جاري تحميل الفروع...",
|
||||
"fr": "Chargement des branches...",
|
||||
"tr": "Dallar yükleniyor...",
|
||||
"de": "Lade Branches..."
|
||||
},
|
||||
"HOME$FAILED_TO_LOAD_BRANCHES": {
|
||||
"en": "Failed to load branches",
|
||||
"ja": "ブランチの読み込みに失敗しました",
|
||||
"zh-CN": "加载分支失败",
|
||||
"zh-TW": "加載分支失敗",
|
||||
"ko-KR": "브랜치 로딩 실패",
|
||||
"no": "Kunne ikke laste inn branches",
|
||||
"it": "Impossibile caricare i branch",
|
||||
"pt": "Falha ao carregar branches",
|
||||
"es": "Error al cargar ramas",
|
||||
"ar": "فشل في تحميل الفروع",
|
||||
"fr": "Échec du chargement des branches",
|
||||
"tr": "Dallar yüklenemedi",
|
||||
"de": "Fehler beim Laden der Branches"
|
||||
},
|
||||
"HOME$OPEN_ISSUE": {
|
||||
"en": "Open issue",
|
||||
"ja": "オープンな課題",
|
||||
@@ -1052,21 +1022,6 @@
|
||||
"fr": "Notifications sonores",
|
||||
"tr": "Ses Bildirimleri"
|
||||
},
|
||||
"SETTINGS$PROACTIVE_CONVERSATION_STARTERS": {
|
||||
"en": "Suggest Tasks on GitHub",
|
||||
"ja": "GitHubでタスクを提案",
|
||||
"zh-CN": "在GitHub上推荐任务",
|
||||
"zh-TW": "在GitHub上推薦任務",
|
||||
"ko-KR": "GitHub에서 작업 제안",
|
||||
"de": "Aufgaben auf GitHub vorschlagen",
|
||||
"no": "Foreslå oppgaver på GitHub",
|
||||
"it": "Suggerisci attività su GitHub",
|
||||
"pt": "Sugerir tarefas no GitHub",
|
||||
"es": "Sugerir tareas en GitHub",
|
||||
"ar": "اقتراح المهام على GitHub",
|
||||
"fr": "Suggérer des tâches sur GitHub",
|
||||
"tr": "GitHub'da Görevler Öner"
|
||||
},
|
||||
"SETTINGS$CUSTOM_MODEL": {
|
||||
"en": "Custom Model",
|
||||
"ja": "カスタムモデル",
|
||||
@@ -4433,21 +4388,6 @@
|
||||
"tr": "Çalışma alanı ayarlanıyor...",
|
||||
"ja": "ワークスペースを設定中..."
|
||||
},
|
||||
"STATUS$SETTING_UP_GIT_HOOKS": {
|
||||
"en": "Setting up git hooks...",
|
||||
"zh-CN": "正在设置 git 钩子...",
|
||||
"zh-TW": "正在設置 git 鉤子...",
|
||||
"de": "Git-Hooks werden eingerichtet...",
|
||||
"ko-KR": "git 훅을 설정하는 중...",
|
||||
"no": "Setter opp git-hooks...",
|
||||
"it": "Configurazione degli hook git...",
|
||||
"pt": "Configurando hooks do git...",
|
||||
"es": "Configurando hooks de git...",
|
||||
"ar": "جاري إعداد خطافات git...",
|
||||
"fr": "Configuration des hooks git...",
|
||||
"tr": "Git kancaları ayarlanıyor...",
|
||||
"ja": "git フックを設定中..."
|
||||
},
|
||||
"ACCOUNT_SETTINGS_MODAL$DISCONNECT": {
|
||||
"en": "Disconnect",
|
||||
"es": "Desconectar",
|
||||
@@ -6877,185 +6817,5 @@
|
||||
"de": "Fehler beim Akzeptieren der Nutzungsbedingungen",
|
||||
"it": "Errore nell'accettazione dei Termini di Servizio",
|
||||
"pt": "Erro ao aceitar os Termos de Serviço"
|
||||
},
|
||||
"TIPS$CUSTOMIZE_MICROAGENT": {
|
||||
"en": "You can customize OpenHands for your repo using a microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",
|
||||
"ja": "マイクロエージェントを使用して、リポジトリ用にOpenHandsをカスタマイズできます。OpenHandsに、コードの実行方法を含むリポジトリの説明を.openhands/microagents/repo.mdに入れるよう依頼してください。",
|
||||
"zh-CN": "您可以使用微代理为您的仓库自定义OpenHands。请OpenHands将仓库的描述(包括如何运行代码)放入.openhands/microagents/repo.md。",
|
||||
"zh-TW": "您可以使用微代理為您的倉庫自定義OpenHands。請OpenHands將倉庫的描述(包括如何運行代碼)放入.openhands/microagents/repo.md。",
|
||||
"ko-KR": "마이크로에이전트를 사용하여 저장소에 맞게 OpenHands를 사용자 정의할 수 있습니다. OpenHands에게 코드 실행 방법을 포함한 저장소 설명을 .openhands/microagents/repo.md에 넣도록 요청하세요.",
|
||||
"no": "Du kan tilpasse OpenHands for ditt repo ved å bruke en mikroagent. Be OpenHands om å legge en beskrivelse av repoet, inkludert hvordan du kjører koden, i .openhands/microagents/repo.md.",
|
||||
"it": "Puoi personalizzare OpenHands per il tuo repository utilizzando un microagente. Chiedi a OpenHands di inserire una descrizione del repository, incluso come eseguire il codice, in .openhands/microagents/repo.md.",
|
||||
"pt": "Você pode personalizar o OpenHands para seu repositório usando um microagente. Peça ao OpenHands para colocar uma descrição do repositório, incluindo como executar o código, em .openhands/microagents/repo.md.",
|
||||
"es": "Puede personalizar OpenHands para su repositorio utilizando un microagente. Pídale a OpenHands que ponga una descripción del repositorio, incluido cómo ejecutar el código, en .openhands/microagents/repo.md.",
|
||||
"ar": "يمكنك تخصيص OpenHands لمستودعك باستخدام وكيل مصغر. اطلب من OpenHands وضع وصف للمستودع، بما في ذلك كيفية تشغيل الكود، في .openhands/microagents/repo.md.",
|
||||
"fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.",
|
||||
"tr": "Bir mikro ajan kullanarak deponuz için OpenHands'i özelleştirebilirsiniz. OpenHands'ten kodun nasıl çalıştırılacağı da dahil olmak üzere deponun açıklamasını .openhands/microagents/repo.md dosyasına koymasını isteyin.",
|
||||
"de": "Sie können OpenHands für Ihr Repository mit einem Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren."
|
||||
},
|
||||
"TIPS$SETUP_SCRIPT": {
|
||||
"en": "You can add .openhands/setup.sh to your repository to automatically run a setup script every time you start an OpenHands conversation.",
|
||||
"ja": "OpenHandsの会話を開始するたびに自動的にセットアップスクリプトを実行するために、.openhands/setup.shをリポジトリに追加できます。",
|
||||
"zh-CN": "您可以将.openhands/setup.sh添加到您的仓库中,以便在每次启动OpenHands对话时自动运行设置脚本。",
|
||||
"zh-TW": "您可以將.openhands/setup.sh添加到您的倉庫中,以便在每次啟動OpenHands對話時自動運行設置腳本。",
|
||||
"ko-KR": "OpenHands 대화를 시작할 때마다 자동으로 설정 스크립트를 실행하도록 .openhands/setup.sh를 저장소에 추가할 수 있습니다.",
|
||||
"no": "Du kan legge til .openhands/setup.sh i ditt repository for å automatisk kjøre et oppsettskript hver gang du starter en OpenHands-samtale.",
|
||||
"it": "Puoi aggiungere .openhands/setup.sh al tuo repository per eseguire automaticamente uno script di configurazione ogni volta che avvii una conversazione OpenHands.",
|
||||
"pt": "Você pode adicionar .openhands/setup.sh ao seu repositório para executar automaticamente um script de configuração toda vez que iniciar uma conversa OpenHands.",
|
||||
"es": "Puede agregar .openhands/setup.sh a su repositorio para ejecutar automáticamente un script de configuración cada vez que inicie una conversación de OpenHands.",
|
||||
"ar": "يمكنك إضافة .openhands/setup.sh إلى مستودعك لتشغيل نص إعداد تلقائيًا في كل مرة تبدأ فيها محادثة OpenHands.",
|
||||
"fr": "Vous pouvez ajouter .openhands/setup.sh à votre dépôt pour exécuter automatiquement un script de configuration chaque fois que vous démarrez une conversation OpenHands.",
|
||||
"tr": "OpenHands konuşması başlattığınız her seferinde otomatik olarak bir kurulum betiği çalıştırmak için deponuza .openhands/setup.sh ekleyebilirsiniz.",
|
||||
"de": "Sie können .openhands/setup.sh zu Ihrem Repository hinzufügen, um jedes Mal, wenn Sie ein OpenHands-Gespräch starten, automatisch ein Setup-Skript auszuführen."
|
||||
},
|
||||
"TIPS$VSCODE_INSTANCE": {
|
||||
"en": "Every OpenHands conversation comes with a VS Code instance, where you can interact with the development environment.",
|
||||
"ja": "すべてのOpenHands会話には、開発環境と対話できるVS Codeインスタンスが付属しています。",
|
||||
"zh-CN": "每个OpenHands对话都配有VS Code实例,您可以在其中与开发环境交互。",
|
||||
"zh-TW": "每個OpenHands對話都配有VS Code實例,您可以在其中與開發環境交互。",
|
||||
"ko-KR": "모든 OpenHands 대화에는 개발 환경과 상호 작용할 수 있는 VS Code 인스턴스가 함께 제공됩니다.",
|
||||
"no": "Hver OpenHands-samtale kommer med en VS Code-instans, hvor du kan samhandle med utviklingsmiljøet.",
|
||||
"it": "Ogni conversazione OpenHands è dotata di un'istanza VS Code, dove puoi interagire con l'ambiente di sviluppo.",
|
||||
"pt": "Cada conversa OpenHands vem com uma instância do VS Code, onde você pode interagir com o ambiente de desenvolvimento.",
|
||||
"es": "Cada conversación de OpenHands viene con una instancia de VS Code, donde puede interactuar con el entorno de desarrollo.",
|
||||
"ar": "تأتي كل محادثة OpenHands مع نسخة من VS Code، حيث يمكنك التفاعل مع بيئة التطوير.",
|
||||
"fr": "Chaque conversation OpenHands est accompagnée d'une instance VS Code, où vous pouvez interagir avec l'environnement de développement.",
|
||||
"tr": "Her OpenHands konuşması, geliştirme ortamıyla etkileşimde bulunabileceğiniz bir VS Code örneği ile birlikte gelir.",
|
||||
"de": "Jedes OpenHands-Gespräch wird mit einer VS Code-Instanz geliefert, in der Sie mit der Entwicklungsumgebung interagieren können."
|
||||
},
|
||||
"TIPS$SAVE_WORK": {
|
||||
"en": "Be sure to regularly save your work, either by pushing to GitHub or by downloading your files via VS Code.",
|
||||
"ja": "GitHubにプッシュするか、VS Codeを介してファイルをダウンロードすることで、定期的に作業を保存してください。",
|
||||
"zh-CN": "请确保定期保存您的工作,可以通过推送到GitHub或通过VS Code下载文件来实现。",
|
||||
"zh-TW": "請確保定期保存您的工作,可以通過推送到GitHub或通過VS Code下載文件來實現。",
|
||||
"ko-KR": "GitHub에 푸시하거나 VS Code를 통해 파일을 다운로드하여 정기적으로 작업을 저장하세요.",
|
||||
"no": "Sørg for å lagre arbeidet ditt regelmessig, enten ved å pushe til GitHub eller ved å laste ned filene dine via VS Code.",
|
||||
"it": "Assicurati di salvare regolarmente il tuo lavoro, sia inviando a GitHub o scaricando i tuoi file tramite VS Code.",
|
||||
"pt": "Certifique-se de salvar regularmente seu trabalho, seja enviando para o GitHub ou baixando seus arquivos via VS Code.",
|
||||
"es": "Asegúrese de guardar regularmente su trabajo, ya sea enviándolo a GitHub o descargando sus archivos a través de VS Code.",
|
||||
"ar": "تأكد من حفظ عملك بانتظام، إما عن طريق الدفع إلى GitHub أو عن طريق تنزيل ملفاتك عبر VS Code.",
|
||||
"fr": "Assurez-vous de sauvegarder régulièrement votre travail, soit en le poussant vers GitHub, soit en téléchargeant vos fichiers via VS Code.",
|
||||
"tr": "GitHub'a göndererek veya VS Code aracılığıyla dosyalarınızı indirerek çalışmalarınızı düzenli olarak kaydettiğinizden emin olun.",
|
||||
"de": "Stellen Sie sicher, dass Sie Ihre Arbeit regelmäßig speichern, entweder durch Pushen zu GitHub oder durch Herunterladen Ihrer Dateien über VS Code."
|
||||
},
|
||||
"TIPS$SPECIFY_FILES": {
|
||||
"en": "When possible, include the names of files or functions OpenHands should focus on. This can help OpenHands work faster, save money, and improve accuracy.",
|
||||
"ja": "可能な場合は、OpenHandsが集中すべきファイルや関数の名前を含めてください。これにより、OpenHandsの作業が速くなり、コストを節約し、精度を向上させることができます。",
|
||||
"zh-CN": "如果可能,请包含OpenHands应该关注的文件或函数的名称。这可以帮助OpenHands更快地工作,节省成本,并提高准确性。",
|
||||
"zh-TW": "如果可能,請包含OpenHands應該關注的文件或函數的名稱。這可以幫助OpenHands更快地工作,節省成本,並提高準確性。",
|
||||
"ko-KR": "가능한 경우, OpenHands가 집중해야 할 파일이나 함수의 이름을 포함하세요. 이는 OpenHands가 더 빠르게 작업하고, 비용을 절약하며, 정확도를 향상시키는 데 도움이 됩니다.",
|
||||
"no": "Når det er mulig, inkluder navnene på filer eller funksjoner OpenHands bør fokusere på. Dette kan hjelpe OpenHands å jobbe raskere, spare penger og forbedre nøyaktigheten.",
|
||||
"it": "Quando possibile, includi i nomi dei file o delle funzioni su cui OpenHands dovrebbe concentrarsi. Questo può aiutare OpenHands a lavorare più velocemente, risparmiare denaro e migliorare la precisione.",
|
||||
"pt": "Quando possível, inclua os nomes dos arquivos ou funções nos quais o OpenHands deve se concentrar. Isso pode ajudar o OpenHands a trabalhar mais rápido, economizar dinheiro e melhorar a precisão.",
|
||||
"es": "Cuando sea posible, incluya los nombres de los archivos o funciones en los que OpenHands debe centrarse. Esto puede ayudar a OpenHands a trabajar más rápido, ahorrar dinero y mejorar la precisión.",
|
||||
"ar": "عندما يكون ذلك ممكنًا، قم بتضمين أسماء الملفات أو الوظائف التي يجب أن يركز عليها OpenHands. يمكن أن يساعد ذلك OpenHands على العمل بشكل أسرع، وتوفير المال، وتحسين الدقة.",
|
||||
"fr": "Lorsque c'est possible, incluez les noms des fichiers ou des fonctions sur lesquels OpenHands devrait se concentrer. Cela peut aider OpenHands à travailler plus rapidement, à économiser de l'argent et à améliorer la précision.",
|
||||
"tr": "Mümkün olduğunda, OpenHands'in odaklanması gereken dosya veya fonksiyon isimlerini dahil edin. Bu, OpenHands'in daha hızlı çalışmasına, para tasarrufu sağlamasına ve doğruluğu artırmasına yardımcı olabilir.",
|
||||
"de": "Wenn möglich, geben Sie die Namen der Dateien oder Funktionen an, auf die sich OpenHands konzentrieren soll. Dies kann OpenHands helfen, schneller zu arbeiten, Geld zu sparen und die Genauigkeit zu verbessern."
|
||||
},
|
||||
"TIPS$HEADLESS_MODE": {
|
||||
"en": "You can run OpenHands in headless mode to create automations, like responding to 500 errors by automatically creating a fix.",
|
||||
"ja": "OpenHandsをヘッドレスモードで実行して、500エラーに対して自動的に修正を作成するなどの自動化を作成できます。",
|
||||
"zh-CN": "您可以在无头模式下运行OpenHands来创建自动化,例如通过自动创建修复来响应500错误。",
|
||||
"zh-TW": "您可以在無頭模式下運行OpenHands來創建自動化,例如通過自動創建修復來響應500錯誤。",
|
||||
"ko-KR": "OpenHands를 헤드리스 모드로 실행하여 500 오류에 자동으로 수정을 생성하는 등의 자동화를 만들 수 있습니다.",
|
||||
"no": "Du kan kjøre OpenHands i headless-modus for å lage automatiseringer, som å svare på 500-feil ved å automatisk opprette en løsning.",
|
||||
"it": "Puoi eseguire OpenHands in modalità headless per creare automazioni, come rispondere agli errori 500 creando automaticamente una correzione.",
|
||||
"pt": "Você pode executar o OpenHands no modo headless para criar automações, como responder a erros 500 criando automaticamente uma correção.",
|
||||
"es": "Puede ejecutar OpenHands en modo headless para crear automatizaciones, como responder a errores 500 creando automáticamente una solución.",
|
||||
"ar": "يمكنك تشغيل OpenHands في الوضع اللارأسي لإنشاء عمليات آلية، مثل الاستجابة لأخطاء 500 عن طريق إنشاء إصلاح تلقائيًا.",
|
||||
"fr": "Vous pouvez exécuter OpenHands en mode headless pour créer des automatisations, comme répondre aux erreurs 500 en créant automatiquement un correctif.",
|
||||
"tr": "OpenHands'i başsız modda çalıştırarak, 500 hatalarına otomatik olarak düzeltme oluşturarak yanıt vermek gibi otomasyonlar oluşturabilirsiniz.",
|
||||
"de": "Sie können OpenHands im Headless-Modus ausführen, um Automatisierungen zu erstellen, wie z.B. das Reagieren auf 500-Fehler durch automatisches Erstellen einer Lösung."
|
||||
},
|
||||
"TIPS$CLI_MODE": {
|
||||
"en": "You can run OpenHands as a CLI, similar to Claude Code.",
|
||||
"ja": "Claude Codeと同様に、OpenHandsをCLIとして実行できます。",
|
||||
"zh-CN": "您可以将OpenHands作为CLI运行,类似于Claude Code。",
|
||||
"zh-TW": "您可以將OpenHands作為CLI運行,類似於Claude Code。",
|
||||
"ko-KR": "Claude Code와 유사하게 OpenHands를 CLI로 실행할 수 있습니다.",
|
||||
"no": "Du kan kjøre OpenHands som en CLI, lignende Claude Code.",
|
||||
"it": "Puoi eseguire OpenHands come CLI, simile a Claude Code.",
|
||||
"pt": "Você pode executar o OpenHands como CLI, semelhante ao Claude Code.",
|
||||
"es": "Puede ejecutar OpenHands como CLI, similar a Claude Code.",
|
||||
"ar": "يمكنك تشغيل OpenHands كواجهة سطر أوامر، مشابهة لـ Claude Code.",
|
||||
"fr": "Vous pouvez exécuter OpenHands en tant que CLI, similaire à Claude Code.",
|
||||
"tr": "OpenHands'i Claude Code'a benzer şekilde bir CLI olarak çalıştırabilirsiniz.",
|
||||
"de": "Sie können OpenHands als CLI ausführen, ähnlich wie Claude Code."
|
||||
},
|
||||
"TIPS$GITHUB_HOOK": {
|
||||
"en": "OpenHands Cloud offers a GitHub hook, so you can say \"@openhands fix the merge conflicts\" or \"@openhands fix the feedback on this PR\" right inside the GitHub UI.",
|
||||
"ja": "OpenHands CloudはGitHubフックを提供しているため、GitHub UI内で「@openhands マージの競合を修正して」や「@openhands このPRのフィードバックを修正して」と言うことができます。",
|
||||
"zh-CN": "OpenHands Cloud提供GitHub钩子,因此您可以在GitHub UI中直接说\"@openhands 修复合并冲突\"或\"@openhands 修复此PR上的反馈\"。",
|
||||
"zh-TW": "OpenHands Cloud提供GitHub鉤子,因此您可以在GitHub UI中直接說\"@openhands 修復合併衝突\"或\"@openhands 修復此PR上的反饋\"。",
|
||||
"ko-KR": "OpenHands Cloud는 GitHub 훅을 제공하므로 GitHub UI 내에서 \"@openhands 병합 충돌 수정\" 또는 \"@openhands 이 PR의 피드백 수정\"이라고 말할 수 있습니다.",
|
||||
"no": "OpenHands Cloud tilbyr en GitHub-hook, så du kan si \"@openhands fix the merge conflicts\" eller \"@openhands fix the feedback on this PR\" direkte i GitHub-grensesnittet.",
|
||||
"it": "OpenHands Cloud offre un hook GitHub, così puoi dire \"@openhands fix the merge conflicts\" o \"@openhands fix the feedback on this PR\" direttamente nell'interfaccia di GitHub.",
|
||||
"pt": "O OpenHands Cloud oferece um hook do GitHub, para que você possa dizer \"@openhands fix the merge conflicts\" ou \"@openhands fix the feedback on this PR\" diretamente na interface do GitHub.",
|
||||
"es": "OpenHands Cloud ofrece un hook de GitHub, por lo que puede decir \"@openhands fix the merge conflicts\" o \"@openhands fix the feedback on this PR\" directamente en la interfaz de GitHub.",
|
||||
"ar": "يوفر OpenHands Cloud خطافًا لـ GitHub، لذلك يمكنك قول \"@openhands أصلح تعارضات الدمج\" أو \"@openhands أصلح التعليقات على طلب السحب هذا\" مباشرة داخل واجهة GitHub.",
|
||||
"fr": "OpenHands Cloud propose un hook GitHub, vous pouvez donc dire \"@openhands fix the merge conflicts\" ou \"@openhands fix the feedback on this PR\" directement dans l'interface GitHub.",
|
||||
"tr": "OpenHands Cloud, GitHub kancası sunar, böylece GitHub arayüzünde doğrudan \"@openhands birleştirme çakışmalarını düzelt\" veya \"@openhands bu PR'daki geri bildirimi düzelt\" diyebilirsiniz.",
|
||||
"de": "OpenHands Cloud bietet einen GitHub-Hook, sodass Sie \"@openhands fix the merge conflicts\" oder \"@openhands fix the feedback on this PR\" direkt in der GitHub-Benutzeroberfläche sagen können."
|
||||
},
|
||||
"TIPS$BLOG_SIGNUP": {
|
||||
"en": "Sign up for the OpenHands Blog to hear about new features and the latest releases.",
|
||||
"ja": "OpenHandsブログに登録して、新機能や最新リリースについての情報を入手しましょう。",
|
||||
"zh-CN": "注册OpenHands博客,了解新功能和最新版本。",
|
||||
"zh-TW": "註冊OpenHands博客,了解新功能和最新版本。",
|
||||
"ko-KR": "OpenHands 블로그에 가입하여 새로운 기능과 최신 릴리스에 대한 정보를 받아보세요.",
|
||||
"no": "Meld deg på OpenHands-bloggen for å høre om nye funksjoner og de nyeste utgivelsene.",
|
||||
"it": "Iscriviti al Blog di OpenHands per conoscere le nuove funzionalità e gli ultimi rilasci.",
|
||||
"pt": "Inscreva-se no Blog do OpenHands para ouvir sobre novos recursos e as últimas versões.",
|
||||
"es": "Suscríbase al Blog de OpenHands para conocer las nuevas funciones y las últimas versiones.",
|
||||
"ar": "اشترك في مدونة OpenHands للاطلاع على الميزات الجديدة وأحدث الإصدارات",
|
||||
"fr": "Inscrivez-vous au blog OpenHands pour connaître les nouvelles fonctionnalités et les dernières versions.",
|
||||
"tr": "Yeni özellikler ve en son sürümler hakkında bilgi almak için OpenHands Blog'a kaydolun.",
|
||||
"de": "Melden Sie sich für den OpenHands Blog an, um über neue Funktionen und die neuesten Versionen informiert zu werden."
|
||||
},
|
||||
"TIPS$API_USAGE": {
|
||||
"en": "OpenHands has an API! Create OpenHands conversations with simple cURL command.",
|
||||
"ja": "OpenHandsにはAPIがあります!簡単なcURLコマンドでOpenHands会話を作成できます。",
|
||||
"zh-CN": "OpenHands有API!使用简单的cURL命令创建OpenHands对话。",
|
||||
"zh-TW": "OpenHands有API!使用簡單的cURL命令創建OpenHands對話。",
|
||||
"ko-KR": "OpenHands에는 API가 있습니다! 간단한 cURL 명령으로 OpenHands 대화를 만들 수 있습니다.",
|
||||
"no": "OpenHands har et API! Opprett OpenHands-samtaler med enkel cURL-kommando.",
|
||||
"it": "OpenHands ha un'API! Crea conversazioni OpenHands con un semplice comando cURL.",
|
||||
"pt": "OpenHands tem uma API! Crie conversas OpenHands com um simples comando cURL.",
|
||||
"es": "¡OpenHands tiene una API! Cree conversaciones de OpenHands con un simple comando cURL.",
|
||||
"ar": "OpenHands لديه واجهة برمجة تطبيقات! قم بإنشاء محادثات OpenHands باستخدام أمر cURL بسيط.",
|
||||
"fr": "OpenHands a une API ! Créez des conversations OpenHands avec une simple commande cURL.",
|
||||
"tr": "OpenHands'in bir API'si var! Basit bir cURL komutuyla OpenHands konuşmaları oluşturun.",
|
||||
"de": "OpenHands hat eine API! Erstellen Sie OpenHands-Gespräche mit einem einfachen cURL-Befehl."
|
||||
},
|
||||
"TIPS$LEARN_MORE": {
|
||||
"en": "Learn more",
|
||||
"ja": "詳細を見る",
|
||||
"zh-CN": "了解更多",
|
||||
"zh-TW": "了解更多",
|
||||
"ko-KR": "더 알아보기",
|
||||
"no": "Lær mer",
|
||||
"it": "Scopri di più",
|
||||
"pt": "Saiba mais",
|
||||
"es": "Más información",
|
||||
"ar": "اعرف المزيد",
|
||||
"fr": "En savoir plus",
|
||||
"tr": "Daha fazla bilgi",
|
||||
"de": "Mehr erfahren"
|
||||
},
|
||||
"TIPS$PROTIP": {
|
||||
"en": "Protip",
|
||||
"ja": "プロのヒント",
|
||||
"zh-CN": "专业提示",
|
||||
"zh-TW": "專業提示",
|
||||
"ko-KR": "프로팁",
|
||||
"no": "Proffetips",
|
||||
"it": "Consiglio pro",
|
||||
"pt": "Dica profissional",
|
||||
"es": "Consejo profesional",
|
||||
"ar": "نصيحة احترافية",
|
||||
"fr": "Astuce pro",
|
||||
"tr": "Uzman ipucu",
|
||||
"de": "Profi-Tipp"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
provider_tokens_set: DEFAULT_SETTINGS.PROVIDER_TOKENS_SET,
|
||||
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
|
||||
enable_proactive_conversation_starters:
|
||||
DEFAULT_SETTINGS.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
|
||||
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default [
|
||||
route("api-keys", "routes/api-keys.tsx"),
|
||||
]),
|
||||
route("conversations/:conversationId", "routes/conversation.tsx", [
|
||||
index("routes/changes-tab.tsx"),
|
||||
index("routes/editor.tsx"),
|
||||
route("browser", "routes/browser-tab.tsx"),
|
||||
route("jupyter", "routes/jupyter-tab.tsx"),
|
||||
route("served", "routes/served-tab.tsx"),
|
||||
|
||||
@@ -15,14 +15,12 @@ import {
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
import { AppSettingsInputsSkeleton } from "#/components/features/settings/app-settings/app-settings-inputs-skeleton";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
function AppSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { mutate: saveSettings, isPending } = useSaveSettings();
|
||||
const { data: settings, isLoading } = useSettings();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const [languageInputHasChanged, setLanguageInputHasChanged] =
|
||||
React.useState(false);
|
||||
@@ -32,10 +30,6 @@ function AppSettingsScreen() {
|
||||
soundNotificationsSwitchHasChanged,
|
||||
setSoundNotificationsSwitchHasChanged,
|
||||
] = React.useState(false);
|
||||
const [
|
||||
proactiveConversationsSwitchHasChanged,
|
||||
setProactiveConversationsSwitchHasChanged,
|
||||
] = React.useState(false);
|
||||
|
||||
const formAction = (formData: FormData) => {
|
||||
const languageLabel = formData.get("language-input")?.toString();
|
||||
@@ -49,16 +43,11 @@ function AppSettingsScreen() {
|
||||
const enableSoundNotifications =
|
||||
formData.get("enable-sound-notifications-switch")?.toString() === "on";
|
||||
|
||||
const enableProactiveConversations =
|
||||
formData.get("enable-proactive-conversations-switch")?.toString() ===
|
||||
"on";
|
||||
|
||||
saveSettings(
|
||||
{
|
||||
LANGUAGE: language,
|
||||
user_consents_to_analytics: enableAnalytics,
|
||||
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
|
||||
ENABLE_PROACTIVE_CONVERSATION_STARTERS: enableProactiveConversations,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -101,19 +90,10 @@ function AppSettingsScreen() {
|
||||
);
|
||||
};
|
||||
|
||||
const checkIfProactiveConversationsSwitchHasChanged = (checked: boolean) => {
|
||||
const currentProactiveConversations =
|
||||
!!settings?.ENABLE_PROACTIVE_CONVERSATION_STARTERS;
|
||||
setProactiveConversationsSwitchHasChanged(
|
||||
checked !== currentProactiveConversations,
|
||||
);
|
||||
};
|
||||
|
||||
const formIsClean =
|
||||
!languageInputHasChanged &&
|
||||
!analyticsSwitchHasChanged &&
|
||||
!soundNotificationsSwitchHasChanged &&
|
||||
!proactiveConversationsSwitchHasChanged;
|
||||
!soundNotificationsSwitchHasChanged;
|
||||
|
||||
const shouldBeLoading = !settings || isLoading || isPending;
|
||||
|
||||
@@ -149,19 +129,6 @@ function AppSettingsScreen() {
|
||||
>
|
||||
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
|
||||
</SettingsSwitch>
|
||||
|
||||
{config?.APP_MODE === "saas" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-proactive-conversations-switch"
|
||||
name="enable-proactive-conversations-switch"
|
||||
defaultIsToggled={
|
||||
!!settings.ENABLE_PROACTIVE_CONVERSATION_STARTERS
|
||||
}
|
||||
onToggle={checkIfProactiveConversationsSwitchHasChanged}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$PROACTIVE_CONVERSATION_STARTERS)}
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDisclosure } from "@heroui/react";
|
||||
import React from "react";
|
||||
import { Outlet, useNavigate } from "react-router";
|
||||
import { Outlet } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FaServer, FaExternalLinkAlt } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -16,6 +16,7 @@ import { Controls } from "#/components/features/controls/controls";
|
||||
import { clearMessages, addUserMessage } from "#/state/chat-slice";
|
||||
import { clearTerminal } from "#/state/command-slice";
|
||||
import { useEffectOnce } from "#/hooks/use-effect-once";
|
||||
|
||||
import GlobeIcon from "#/icons/globe.svg?react";
|
||||
import JupyterIcon from "#/icons/jupyter.svg?react";
|
||||
import TerminalIcon from "#/icons/terminal.svg?react";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
ResizablePanel,
|
||||
} from "#/components/layout/resizable-panel";
|
||||
import Security from "#/components/shared/modals/security/security";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
import { ServedAppLabel } from "#/components/layout/served-app-label";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
@@ -39,7 +41,6 @@ import { RootState } from "#/store";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
import { TabContent } from "#/components/layout/tab-content";
|
||||
|
||||
function AppContent() {
|
||||
useConversationConfig();
|
||||
@@ -54,7 +55,7 @@ function AppContent() {
|
||||
);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const endSession = useEndSession();
|
||||
|
||||
// Set the document title to the conversation title when available
|
||||
useDocumentTitleFromState();
|
||||
@@ -66,7 +67,7 @@ function AppContent() {
|
||||
displayErrorToast(
|
||||
"This conversation does not exist, or you do not have permission to access it.",
|
||||
);
|
||||
navigate("/");
|
||||
endSession();
|
||||
}
|
||||
}, [conversation, isFetched]);
|
||||
|
||||
@@ -112,8 +113,6 @@ function AppContent() {
|
||||
} = useDisclosure();
|
||||
|
||||
function renderMain() {
|
||||
const basePath = `/conversations/${conversationId}`;
|
||||
|
||||
if (width <= 640) {
|
||||
return (
|
||||
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full bg-base-secondary">
|
||||
@@ -198,15 +197,7 @@ function AppContent() {
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* Use both Outlet and TabContent */}
|
||||
<div className="h-full w-full">
|
||||
{/* Keep the Outlet for React Router to work properly */}
|
||||
<div className="hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
{/* Use TabContent to keep all tabs loaded but only show the active one */}
|
||||
<TabContent conversationPath={basePath} />
|
||||
</div>
|
||||
<Outlet />
|
||||
</Container>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import React from "react";
|
||||
import { FileDiffViewer } from "#/components/features/diff-viewer/file-diff-viewer";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
import { useGetGitChanges } from "#/hooks/query/use-get-git-changes";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { RandomTip } from "#/components/features/tips/random-tip";
|
||||
|
||||
// Error message patterns
|
||||
const GIT_REPO_ERROR_PATTERN = /not a git repository/i;
|
||||
@@ -20,7 +18,7 @@ function StatusMessage({ children }: React.PropsWithChildren) {
|
||||
);
|
||||
}
|
||||
|
||||
function GitChanges() {
|
||||
function EditorScreen() {
|
||||
const { t } = useTranslation();
|
||||
const { data: gitChanges, isSuccess, isError, error } = useGetGitChanges();
|
||||
|
||||
@@ -30,50 +28,37 @@ function GitChanges() {
|
||||
const isNotGitRepoError =
|
||||
error && GIT_REPO_ERROR_PATTERN.test(retrieveAxiosErrorMessage(error));
|
||||
|
||||
let statusMessage: React.ReactNode = null;
|
||||
if (!runtimeIsActive) {
|
||||
statusMessage = <span>{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}</span>;
|
||||
} else if (isNotGitRepoError) {
|
||||
if (error) {
|
||||
statusMessage = <span>{retrieveAxiosErrorMessage(error)}</span>;
|
||||
} else {
|
||||
statusMessage = (
|
||||
<span>
|
||||
return (
|
||||
<main className="h-full overflow-y-scroll px-4 py-3 gap-3 flex flex-col">
|
||||
{!runtimeIsActive && (
|
||||
<StatusMessage>
|
||||
{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}
|
||||
</StatusMessage>
|
||||
)}
|
||||
{!isNotGitRepoError && error && (
|
||||
<StatusMessage>{retrieveAxiosErrorMessage(error)}</StatusMessage>
|
||||
)}
|
||||
{isNotGitRepoError && (
|
||||
<StatusMessage>
|
||||
{t(I18nKey.DIFF_VIEWER$NOT_A_GIT_REPO)}
|
||||
<br />
|
||||
{t(I18nKey.DIFF_VIEWER$ASK_OH)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
</StatusMessage>
|
||||
)}
|
||||
|
||||
return (
|
||||
<main className="h-full overflow-y-scroll px-4 py-3 gap-3 flex flex-col items-center">
|
||||
{!isSuccess || !gitChanges.length ? (
|
||||
<div className="relative flex h-full w-full items-center">
|
||||
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2">
|
||||
{statusMessage && <StatusMessage>{statusMessage}</StatusMessage>}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0">
|
||||
{!isError && gitChanges?.length === 0 && (
|
||||
<div className="max-w-2xl mb-4 text-m bg-tertiary rounded-xl p-4 text-left mx-auto">
|
||||
<RandomTip />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
{runtimeIsActive && !isError && gitChanges?.length === 0 && (
|
||||
<StatusMessage>{t(I18nKey.DIFF_VIEWER$NO_CHANGES)}</StatusMessage>
|
||||
)}
|
||||
{isSuccess &&
|
||||
gitChanges.map((change) => (
|
||||
<FileDiffViewer
|
||||
key={change.path}
|
||||
path={change.path}
|
||||
type={change.status}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitChanges;
|
||||
export default EditorScreen;
|
||||
@@ -22,7 +22,6 @@ import { useConfig } from "#/hooks/query/use-config";
|
||||
import { isCustomModel } from "#/utils/is-custom-model";
|
||||
import { LlmSettingsInputsSkeleton } from "#/components/features/settings/llm-settings/llm-settings-inputs-skeleton";
|
||||
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
function LlmSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -102,13 +101,6 @@ function LlmSettingsScreen() {
|
||||
{
|
||||
LLM_MODEL: fullLlmModel,
|
||||
llm_api_key: apiKey || null,
|
||||
|
||||
// reset advanced settings
|
||||
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
AGENT: DEFAULT_SETTINGS.AGENT,
|
||||
CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
SECURITY_ANALYZER: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
ENABLE_DEFAULT_CONDENSER: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
},
|
||||
{
|
||||
onSuccess: handleSuccessfulMutation,
|
||||
@@ -134,7 +126,7 @@ function LlmSettingsScreen() {
|
||||
{
|
||||
LLM_MODEL: model,
|
||||
LLM_BASE_URL: baseUrl,
|
||||
llm_api_key: apiKey || null,
|
||||
llm_api_key: apiKey,
|
||||
AGENT: agent,
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Outlet,
|
||||
useNavigate,
|
||||
useLocation,
|
||||
useSearchParams,
|
||||
} from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
@@ -59,8 +60,9 @@ export default function MainApp() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const tosPageStatus = useIsOnTosPage();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: settings } = useSettings();
|
||||
const { error } = useBalance();
|
||||
const { error, isFetching } = useBalance();
|
||||
const { migrateUserConsent } = useMigrateUserConsent();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -111,19 +113,22 @@ export default function MainApp() {
|
||||
}
|
||||
}, [tosPageStatus]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings?.IS_NEW_USER && config.data?.APP_MODE === "saas") {
|
||||
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
|
||||
}
|
||||
}, [settings?.IS_NEW_USER, config.data?.APP_MODE]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Don't do any redirects when on TOS page
|
||||
// Don't allow users to use the app if it 402s
|
||||
if (!tosPageStatus && error?.status === 402 && pathname !== "/") {
|
||||
navigate("/");
|
||||
if (!tosPageStatus) {
|
||||
// Don't allow users to use the app if it 402s
|
||||
if (error?.status === 402 && pathname !== "/") {
|
||||
navigate("/");
|
||||
} else if (
|
||||
!isFetching &&
|
||||
searchParams.get("free_credits") === "success"
|
||||
) {
|
||||
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
|
||||
searchParams.delete("free_credits");
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
}, [error?.status, pathname, tosPageStatus]);
|
||||
}, [error?.status, pathname, isFetching, tosPageStatus]);
|
||||
|
||||
// When on TOS page, we don't make any API calls, so we need to handle this case
|
||||
const userIsAuthed = tosPageStatus ? false : !!isAuthed && !authError;
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
|
||||
function VSCodeTab() {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, error } = useVSCodeUrl();
|
||||
const { conversationId } = useConversation();
|
||||
const [vsCodeUrl, setVsCodeUrl] = React.useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchVSCodeUrl() {
|
||||
if (!conversationId || isRuntimeInactive) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(
|
||||
`/api/conversations/${conversationId}/vscode-url`,
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.vscode_url) {
|
||||
const transformedUrl = transformVSCodeUrl(data.vscode_url);
|
||||
setVsCodeUrl(transformedUrl);
|
||||
} else {
|
||||
setError(t(I18nKey.VSCODE$URL_NOT_AVAILABLE));
|
||||
}
|
||||
} catch (err) {
|
||||
setError(t(I18nKey.VSCODE$FETCH_ERROR));
|
||||
// Error is handled by setting the error state
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchVSCodeUrl();
|
||||
}, [conversationId, isRuntimeInactive, t]);
|
||||
|
||||
if (isRuntimeInactive) {
|
||||
return (
|
||||
@@ -29,10 +60,10 @@ function VSCodeTab() {
|
||||
);
|
||||
}
|
||||
|
||||
if (error || (data && data.error) || !data?.url) {
|
||||
if (error || !vsCodeUrl) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{data?.error || String(error) || t(I18nKey.VSCODE$URL_NOT_AVAILABLE)}
|
||||
{error || t(I18nKey.VSCODE$URL_NOT_AVAILABLE)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,9 +71,8 @@ function VSCodeTab() {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title={t(I18nKey.VSCODE$TITLE)}
|
||||
src={data.url}
|
||||
src={vsCodeUrl}
|
||||
className="w-full h-full border-0"
|
||||
allow={t(I18nKey.VSCODE$IFRAME_PERMISSIONS)}
|
||||
/>
|
||||
@@ -50,5 +80,4 @@ function VSCodeTab() {
|
||||
);
|
||||
}
|
||||
|
||||
// Export the VSCodeTab directly since we're using the provider at a higher level
|
||||
export default VSCodeTab;
|
||||
|
||||
@@ -15,7 +15,6 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
|
||||
IS_NEW_USER: true,
|
||||
};
|
||||
|
||||
|
||||
Vendored
-7
@@ -15,13 +15,6 @@ interface GitUser {
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
interface Branch {
|
||||
name: string;
|
||||
commit_sha: string;
|
||||
protected: boolean;
|
||||
last_push_date?: string;
|
||||
}
|
||||
|
||||
interface GitRepository {
|
||||
id: number;
|
||||
full_name: string;
|
||||
|
||||
@@ -21,7 +21,6 @@ export type Settings = {
|
||||
PROVIDER_TOKENS_SET: Partial<Record<Provider, string | null>>;
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
ENABLE_SOUND_NOTIFICATIONS: boolean;
|
||||
ENABLE_PROACTIVE_CONVERSATION_STARTERS: boolean;
|
||||
USER_CONSENTS_TO_ANALYTICS: boolean | null;
|
||||
IS_NEW_USER?: boolean;
|
||||
};
|
||||
@@ -38,7 +37,6 @@ export type ApiSettings = {
|
||||
remote_runtime_resource_factor: number | null;
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
enable_proactive_conversation_starters: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens_set: Partial<Record<Provider, string | null>>;
|
||||
};
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export interface Tip {
|
||||
key: I18nKey;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
export const TIPS: Tip[] = [
|
||||
{
|
||||
key: I18nKey.TIPS$CUSTOMIZE_MICROAGENT,
|
||||
link: "https://docs.all-hands.dev/modules/usage/prompting/microagents-repo",
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$SETUP_SCRIPT,
|
||||
link: "https://docs.all-hands.dev/modules/usage/customization/repository",
|
||||
},
|
||||
{ key: I18nKey.TIPS$VSCODE_INSTANCE },
|
||||
{ key: I18nKey.TIPS$SAVE_WORK },
|
||||
{
|
||||
key: I18nKey.TIPS$SPECIFY_FILES,
|
||||
link: "https://docs.all-hands.dev/modules/usage/prompting/prompting-best-practices",
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$HEADLESS_MODE,
|
||||
link: "https://docs.all-hands.dev/modules/usage/how-to/headless-mode",
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$CLI_MODE,
|
||||
link: "https://docs.all-hands.dev/modules/usage/how-to/cli-mode",
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$GITHUB_HOOK,
|
||||
link: "https://docs.all-hands.dev/modules/usage/cloud/cloud-github-resolver",
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$BLOG_SIGNUP,
|
||||
link: "https://www.all-hands.dev/blog",
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$API_USAGE,
|
||||
link: "https://docs.all-hands.dev/swagger-ui/",
|
||||
},
|
||||
];
|
||||
|
||||
export function getRandomTip(): Tip {
|
||||
const randomIndex = Math.floor(Math.random() * TIPS.length);
|
||||
return TIPS[randomIndex];
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
import { vi } from "vitest";
|
||||
import { AxiosError } from "axios";
|
||||
import { AppStore, RootState, rootReducer } from "./src/store";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { ConversationProvider } from "#/context/conversation-context";
|
||||
@@ -84,19 +83,3 @@ export function renderWithProviders(
|
||||
}
|
||||
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
|
||||
}
|
||||
|
||||
export const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
+5
-35
@@ -16,51 +16,21 @@ ALWAYS use the GitHub API for operations instead of a web browser.
|
||||
|
||||
If you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`
|
||||
|
||||
## IMPORTANT: ALWAYS USE THE MCP TOOL FOR CREATING PULL REQUESTS
|
||||
|
||||
When creating pull requests, ALWAYS use the MCP (Model Context Protocol) tool instead of directly using the GitHub API. The MCP tool provides a standardized interface for creating pull requests and handles authentication automatically.
|
||||
|
||||
To create a pull request using the MCP tool:
|
||||
1. Push your changes to a branch
|
||||
2. Use the MCP `create_github_pr` tool to create the pull request
|
||||
|
||||
Example of using the MCP tool to create a pull request:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "callTool",
|
||||
"params": {
|
||||
"name": "create_github_pr",
|
||||
"arguments": {
|
||||
"repository": "owner/repo",
|
||||
"title": "Your PR title",
|
||||
"body": "Description of your changes",
|
||||
"head": "your-feature-branch",
|
||||
"base": "main",
|
||||
"draft": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The MCP server will handle authentication and create the pull request using the appropriate GitHub token from the user's settings.
|
||||
|
||||
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
||||
* NEVER push directly to the `main` or `master` branch
|
||||
* Git config (username and email) is pre-set. Do not modify.
|
||||
* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
|
||||
* Use the GitHub API to create a pull request, if you haven't already
|
||||
* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* After opening or updating a pull request, send the user a short message with a link to the pull request.
|
||||
* Prefer "Draft" pull requests when possible
|
||||
* Do NOT mark a pull request as ready to review unless the user explicitly says so
|
||||
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands and then using the MCP tool:
|
||||
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
||||
|
||||
# Then use the MCP tool to create the PR instead of directly using the GitHub API
|
||||
curl -X POST "https://api.github.com/repos/$ORG_NAME/$REPO_NAME/pulls" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-d '{"title":"Create widget","head":"create-widget","base":"openhands-workspace"}'
|
||||
```
|
||||
|
||||
IMPORTANT: NEVER use the GitHub API directly to create pull requests. ALWAYS use the MCP tool.
|
||||
|
||||
+6
-36
@@ -16,50 +16,20 @@ ALWAYS use the GitLab API for operations instead of a web browser.
|
||||
|
||||
If you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://oauth2:${GITLAB_TOKEN}@gitlab.com/username/repo.git`
|
||||
|
||||
## IMPORTANT: ALWAYS USE THE MCP TOOL FOR CREATING MERGE REQUESTS
|
||||
|
||||
When creating merge requests, ALWAYS use the MCP (Model Context Protocol) tool instead of directly using the GitLab API. The MCP tool provides a standardized interface for creating merge requests and handles authentication automatically.
|
||||
|
||||
To create a merge request using the MCP tool:
|
||||
1. Push your changes to a branch
|
||||
2. Use the MCP `create_gitlab_mr` tool to create the merge request
|
||||
|
||||
Example of using the MCP tool to create a merge request:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "callTool",
|
||||
"params": {
|
||||
"name": "create_gitlab_mr",
|
||||
"arguments": {
|
||||
"project_id": "group/project",
|
||||
"title": "Your MR title",
|
||||
"description": "Description of your changes",
|
||||
"source_branch": "your-feature-branch",
|
||||
"target_branch": "main",
|
||||
"draft": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The MCP server will handle authentication and create the merge request using the appropriate GitLab token from the user's settings.
|
||||
|
||||
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
||||
* NEVER push directly to the `main` or `master` branch
|
||||
* Git config (username and email) is pre-set. Do not modify.
|
||||
* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
|
||||
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the MR title and description as necessary, but don't change the branch name.
|
||||
* Use the GitLab API to create a merge request, if you haven't already
|
||||
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* After opening or updating a merge request, send the user a short message with a link to the merge request.
|
||||
* Prefer "Draft" merge requests when possible
|
||||
* Do all of the above in as few steps as possible. E.g. you could open an MR with one step by running the following bash commands and then using the MCP tool:
|
||||
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
||||
|
||||
# Then use the MCP tool to create the MR instead of directly using the GitLab API
|
||||
curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/merge_requests" \
|
||||
-H "Authorization: Bearer $GITLAB_TOKEN" \
|
||||
-d '{"source_branch": "create-widget", "target_branch": "openhands-workspace", "title": "Create widget"}'
|
||||
```
|
||||
|
||||
IMPORTANT: NEVER use the GitLab API directly to create merge requests. ALWAYS use the MCP tool.
|
||||
|
||||
@@ -162,7 +162,7 @@ class BrowsingAgent(Agent):
|
||||
last_action = event
|
||||
elif isinstance(event, MessageAction) and event.source == EventSource.AGENT:
|
||||
# agent has responded, task finished.
|
||||
return AgentFinishAction(outputs={'content': event.content})
|
||||
return AgentFinishAction(final_thought=event.content)
|
||||
elif isinstance(event, Observation):
|
||||
last_obs = event
|
||||
|
||||
@@ -201,10 +201,8 @@ class BrowsingAgent(Agent):
|
||||
)
|
||||
return MessageAction('Error encountered when browsing.')
|
||||
|
||||
goal, _ = state.get_current_user_intent()
|
||||
|
||||
if goal is None:
|
||||
goal = state.inputs['task']
|
||||
user_message_action = state.get_current_user_intent()
|
||||
goal = user_message_action.content
|
||||
|
||||
system_msg = get_system_message(
|
||||
goal,
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import copy
|
||||
import os
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm import ChatCompletionToolParam
|
||||
|
||||
from openhands.events.action import Action
|
||||
from openhands.llm.llm import ModelResponse
|
||||
from litellm import ChatCompletionToolParam
|
||||
|
||||
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
|
||||
from openhands.agenthub.codeact_agent.tools.bash import create_cmd_run_tool
|
||||
@@ -25,7 +20,7 @@ from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.message import Message
|
||||
from openhands.events.action import AgentFinishAction, MessageAction
|
||||
from openhands.events.action import Action, AgentFinishAction, MessageAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.memory.condenser import Condenser
|
||||
@@ -80,26 +75,23 @@ class CodeActAgent(Agent):
|
||||
- config (AgentConfig): The configuration for this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
self.pending_actions: deque['Action'] = deque()
|
||||
self.pending_actions: deque[Action] = deque()
|
||||
self.reset()
|
||||
self.tools = self._get_tools()
|
||||
|
||||
self.prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
)
|
||||
|
||||
# Create a ConversationMemory instance
|
||||
self.conversation_memory = ConversationMemory(self.config, self.prompt_manager)
|
||||
|
||||
self.condenser = Condenser.from_config(self.config.condenser)
|
||||
logger.debug(f'Using condenser: {type(self.condenser)}')
|
||||
|
||||
@property
|
||||
def prompt_manager(self) -> PromptManager:
|
||||
if self._prompt_manager is None:
|
||||
self._prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
)
|
||||
self.response_to_actions_fn = codeact_function_calling.response_to_actions
|
||||
|
||||
return self._prompt_manager
|
||||
|
||||
def _get_tools(self) -> list['ChatCompletionToolParam']:
|
||||
def _get_tools(self) -> list[ChatCompletionToolParam]:
|
||||
# For these models, we use short tool descriptions ( < 1024 tokens)
|
||||
# to avoid hitting the OpenAI token limit for tool descriptions.
|
||||
SHORT_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1', 'o4']
|
||||
@@ -138,7 +130,7 @@ class CodeActAgent(Agent):
|
||||
super().reset()
|
||||
self.pending_actions.clear()
|
||||
|
||||
def step(self, state: State) -> 'Action':
|
||||
def step(self, state: State) -> Action:
|
||||
"""Performs one step using the CodeAct Agent.
|
||||
|
||||
This includes gathering info on previous steps and prompting the model to make a command to execute.
|
||||
@@ -206,7 +198,9 @@ class CodeActAgent(Agent):
|
||||
params['extra_body'] = {'metadata': state.to_llm_metadata(agent_name=self.name)}
|
||||
response = self.llm.completion(**params)
|
||||
logger.debug(f'Response from LLM: {response}')
|
||||
actions = self.response_to_actions(response)
|
||||
actions = self.response_to_actions_fn(
|
||||
response, mcp_tool_names=list(self.mcp_tools.keys())
|
||||
)
|
||||
logger.debug(f'Actions after response_to_actions: {actions}')
|
||||
for action in actions:
|
||||
self.pending_actions.append(action)
|
||||
@@ -280,8 +274,3 @@ class CodeActAgent(Agent):
|
||||
self.conversation_memory.apply_prompt_caching(messages)
|
||||
|
||||
return messages
|
||||
|
||||
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
|
||||
return codeact_function_calling.response_to_actions(
|
||||
response, mcp_tool_names=list(self.mcp_tools.keys())
|
||||
)
|
||||
|
||||
@@ -105,7 +105,8 @@ def response_to_actions(
|
||||
elif tool_call.function.name == 'delegate_to_browsing_agent':
|
||||
action = AgentDelegateAction(
|
||||
agent='BrowsingAgent',
|
||||
inputs=arguments,
|
||||
prompt=arguments.get('prompt', ''),
|
||||
inputs={},
|
||||
)
|
||||
|
||||
# ================================================
|
||||
@@ -113,8 +114,10 @@ def response_to_actions(
|
||||
# ================================================
|
||||
elif tool_call.function.name == FinishTool['function']['name']:
|
||||
action = AgentFinishAction(
|
||||
final_thought=arguments.get('message', ''),
|
||||
outputs=arguments.get('outputs', {}),
|
||||
thought=arguments.get('thought', ''),
|
||||
task_completed=arguments.get('task_completed', None),
|
||||
final_thought=arguments.get('final_thought', ''),
|
||||
)
|
||||
|
||||
# ================================================
|
||||
|
||||
@@ -3,13 +3,6 @@ ReadOnlyAgent - A specialized version of CodeActAgent that only uses read-only t
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm import ChatCompletionToolParam
|
||||
|
||||
from openhands.events.action import Action
|
||||
from openhands.llm.llm import ModelResponse
|
||||
|
||||
from openhands.agenthub.codeact_agent.codeact_agent import CodeActAgent
|
||||
from openhands.agenthub.readonly_agent import (
|
||||
@@ -48,27 +41,24 @@ class ReadOnlyAgent(CodeActAgent):
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
- config (AgentConfig): The configuration for this agent
|
||||
"""
|
||||
# Initialize the CodeActAgent class; some of it is overridden with class methods
|
||||
# Initialize the CodeActAgent class but we'll override some of its behavior
|
||||
super().__init__(llm, config)
|
||||
|
||||
# Override the tools to only include read-only tools
|
||||
# Get the read-only tools from our own function_calling module
|
||||
self.tools = readonly_function_calling.get_tools()
|
||||
|
||||
# Set up our own prompt manager
|
||||
self.prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
)
|
||||
|
||||
self.response_to_actions_fn = readonly_function_calling.response_to_actions
|
||||
|
||||
logger.debug(
|
||||
f"TOOLS loaded for ReadOnlyAgent: {', '.join([tool.get('function').get('name') for tool in self.tools])}"
|
||||
)
|
||||
|
||||
@property
|
||||
def prompt_manager(self) -> PromptManager:
|
||||
# Set up our own prompt manager
|
||||
if self._prompt_manager is None:
|
||||
self._prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
)
|
||||
return self._prompt_manager
|
||||
|
||||
def _get_tools(self) -> list['ChatCompletionToolParam']:
|
||||
# Override the tools to only include read-only tools
|
||||
# Get the read-only tools from our own function_calling module
|
||||
return readonly_function_calling.get_tools()
|
||||
|
||||
def set_mcp_tools(self, mcp_tools: list[dict]) -> None:
|
||||
"""Sets the list of MCP tools for the agent.
|
||||
|
||||
@@ -78,8 +68,3 @@ class ReadOnlyAgent(CodeActAgent):
|
||||
logger.warning(
|
||||
'ReadOnlyAgent does not support MCP tools. MCP tools will be ignored by the agent.'
|
||||
)
|
||||
|
||||
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
|
||||
return readonly_function_calling.response_to_actions(
|
||||
response, mcp_tool_names=list(self.mcp_tools.keys())
|
||||
)
|
||||
|
||||
@@ -216,7 +216,7 @@ Note:
|
||||
last_action = event
|
||||
elif isinstance(event, MessageAction) and event.source == EventSource.AGENT:
|
||||
# agent has responded, task finished.
|
||||
return AgentFinishAction(outputs={'content': event.content})
|
||||
return AgentFinishAction(final_thought=event.content)
|
||||
elif isinstance(event, Observation):
|
||||
# Only process BrowserOutputObservation and skip other observation types
|
||||
if not isinstance(event, BrowserOutputObservation):
|
||||
@@ -271,10 +271,10 @@ Note:
|
||||
)
|
||||
return MessageAction('Error encountered when browsing.')
|
||||
set_of_marks = last_obs.set_of_marks
|
||||
goal, image_urls = state.get_current_user_intent()
|
||||
user_message_action = state.get_current_user_intent()
|
||||
goal = user_message_action.content
|
||||
image_urls = user_message_action.image_urls
|
||||
|
||||
if goal is None:
|
||||
goal = state.inputs['task']
|
||||
goal_txt, goal_images = create_goal_prompt(goal, image_urls)
|
||||
observation_txt, som_screenshot = create_observation_prompt(
|
||||
cur_axtree_txt, tabs, focused_element, error_prefix, set_of_marks
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.events.action import Action
|
||||
from openhands.events.action.message import SystemMessageAction
|
||||
from openhands.utils.prompt import PromptManager
|
||||
from litellm import ChatCompletionToolParam
|
||||
|
||||
from openhands.core.exceptions import (
|
||||
@@ -20,6 +19,9 @@ from openhands.events.event import EventSource
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.utils.prompt import PromptManager
|
||||
|
||||
|
||||
class Agent(ABC):
|
||||
DEPRECATED = False
|
||||
@@ -30,7 +32,7 @@ class Agent(ABC):
|
||||
It tracks the execution status and maintains a history of interactions.
|
||||
"""
|
||||
|
||||
_registry: dict[str, type['Agent']] = {}
|
||||
_registry: dict[str, Type['Agent']] = {}
|
||||
sandbox_plugins: list[PluginRequirement] = []
|
||||
|
||||
def __init__(
|
||||
@@ -41,16 +43,10 @@ class Agent(ABC):
|
||||
self.llm = llm
|
||||
self.config = config
|
||||
self._complete = False
|
||||
self._prompt_manager: 'PromptManager' | None = None
|
||||
self.prompt_manager: 'PromptManager' | None = None
|
||||
self.mcp_tools: dict[str, ChatCompletionToolParam] = {}
|
||||
self.tools: list = []
|
||||
|
||||
@property
|
||||
def prompt_manager(self) -> 'PromptManager':
|
||||
if self._prompt_manager is None:
|
||||
raise ValueError(f'Prompt manager not initialized for agent {self.name}')
|
||||
return self._prompt_manager
|
||||
|
||||
def get_system_message(self) -> 'SystemMessageAction | None':
|
||||
"""
|
||||
Returns a SystemMessageAction containing the system message and tools.
|
||||
@@ -118,7 +114,7 @@ class Agent(ABC):
|
||||
return self.__class__.__name__
|
||||
|
||||
@classmethod
|
||||
def register(cls, name: str, agent_cls: type['Agent']) -> None:
|
||||
def register(cls, name: str, agent_cls: Type['Agent']) -> None:
|
||||
"""Registers an agent class in the registry.
|
||||
|
||||
Parameters:
|
||||
@@ -133,7 +129,7 @@ class Agent(ABC):
|
||||
cls._registry[name] = agent_cls
|
||||
|
||||
@classmethod
|
||||
def get_cls(cls, name: str) -> type['Agent']:
|
||||
def get_cls(cls, name: str) -> Type['Agent']:
|
||||
"""Retrieves an agent class from the registry.
|
||||
|
||||
Parameters:
|
||||
|
||||
@@ -5,7 +5,7 @@ import copy
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from typing import Callable, ClassVar
|
||||
from typing import Callable, ClassVar, Tuple, Type
|
||||
|
||||
import litellm # noqa
|
||||
from litellm.exceptions import ( # noqa
|
||||
@@ -91,7 +91,7 @@ class AgentController:
|
||||
agent_configs: dict[str, AgentConfig]
|
||||
parent: 'AgentController | None' = None
|
||||
delegate: 'AgentController | None' = None
|
||||
_pending_action_info: tuple[Action, float] | None = None # (action, timestamp)
|
||||
_pending_action_info: Tuple[Action, float] | None = None # (action, timestamp)
|
||||
_closed: bool = False
|
||||
filter_out: ClassVar[tuple[type[Event], ...]] = (
|
||||
NullAction,
|
||||
@@ -438,12 +438,13 @@ class AgentController:
|
||||
elif isinstance(action, AgentDelegateAction):
|
||||
await self.start_delegate(action)
|
||||
assert self.delegate is not None
|
||||
# Post a MessageAction with the task for the delegate
|
||||
if 'task' in action.inputs:
|
||||
# Post a MessageAction with the prompt for the delegate
|
||||
if action.prompt:
|
||||
self.event_stream.add_event(
|
||||
MessageAction(content='TASK: ' + action.inputs['task']),
|
||||
EventSource.USER,
|
||||
MessageAction(content=action.prompt),
|
||||
EventSource.USER, # Source is USER, as it represents the task prompt for the delegate
|
||||
)
|
||||
# Delegate starts in RUNNING state as it receives the prompt immediately
|
||||
await self.delegate.set_agent_state_to(AgentState.RUNNING)
|
||||
return
|
||||
|
||||
@@ -675,7 +676,7 @@ class AgentController:
|
||||
Args:
|
||||
action (AgentDelegateAction): The action containing information about the delegate agent to start.
|
||||
"""
|
||||
agent_cls: type[Agent] = Agent.get_cls(action.agent)
|
||||
agent_cls: Type[Agent] = Agent.get_cls(action.agent)
|
||||
agent_config = self.agent_configs.get(action.agent, self.agent.config)
|
||||
llm_config = self.agent_to_llm_config.get(action.agent, self.agent.llm.config)
|
||||
llm = LLM(config=llm_config, retry_listener=self._notify_on_llm_retry)
|
||||
@@ -727,34 +728,22 @@ class AgentController:
|
||||
# close the delegate controller before adding new events
|
||||
asyncio.get_event_loop().run_until_complete(self.delegate.close())
|
||||
|
||||
if delegate_state in (AgentState.FINISHED, AgentState.REJECTED):
|
||||
# retrieve delegate result
|
||||
delegate_outputs = (
|
||||
self.delegate.state.outputs if self.delegate.state else {}
|
||||
)
|
||||
# prepare delegate result observation
|
||||
delegate_outputs = self.delegate.state.outputs if self.delegate.state else {}
|
||||
formatted_output = ', '.join(
|
||||
f'{key}: {value}' for key, value in delegate_outputs.items()
|
||||
)
|
||||
|
||||
# prepare delegate result observation
|
||||
# TODO: replace this with AI-generated summary (#2395)
|
||||
formatted_output = ', '.join(
|
||||
f'{key}: {value}' for key, value in delegate_outputs.items()
|
||||
)
|
||||
if delegate_state in (AgentState.FINISHED, AgentState.REJECTED):
|
||||
content = (
|
||||
f'{self.delegate.agent.name} finishes task with {formatted_output}'
|
||||
)
|
||||
else:
|
||||
# delegate state is ERROR
|
||||
# emit AgentDelegateObservation with error content
|
||||
delegate_outputs = (
|
||||
self.delegate.state.outputs if self.delegate.state else {}
|
||||
)
|
||||
content = (
|
||||
f'{self.delegate.agent.name} encountered an error during execution.'
|
||||
)
|
||||
|
||||
content = f'Delegated agent finished with result:\n\n{content}'
|
||||
content = f'{self.delegate.agent.name} encountered an error during execution. Known results: {delegate_outputs}'
|
||||
|
||||
# emit the delegate result observation
|
||||
obs = AgentDelegateObservation(outputs=delegate_outputs, content=content)
|
||||
obs = AgentDelegateObservation(content=content, outputs={})
|
||||
|
||||
# associate the delegate action with the initiating tool call
|
||||
for event in reversed(self.state.history):
|
||||
|
||||
@@ -188,19 +188,39 @@ class State:
|
||||
if not hasattr(self, 'history'):
|
||||
self.history = []
|
||||
|
||||
def get_current_user_intent(self) -> tuple[str | None, list[str] | None]:
|
||||
"""Returns the latest user message and image(if provided) that appears after a FinishAction, or the first (the task) if nothing was finished yet."""
|
||||
last_user_message = None
|
||||
last_user_message_image_urls: list[str] | None = []
|
||||
for event in reversed(self.view):
|
||||
if isinstance(event, MessageAction) and event.source == 'user':
|
||||
last_user_message = event.content
|
||||
last_user_message_image_urls = event.image_urls
|
||||
elif isinstance(event, AgentFinishAction):
|
||||
if last_user_message is not None:
|
||||
return last_user_message, None
|
||||
def get_current_user_intent(self) -> MessageAction:
|
||||
"""Returns the latest user MessageAction that appears after a FinishAction, or the first (the task) if nothing was finished yet."""
|
||||
likely_task: MessageAction | None = None
|
||||
|
||||
return last_user_message, last_user_message_image_urls
|
||||
# Search in the view for the latest user message after the last finish action
|
||||
for event in reversed(self.view):
|
||||
if isinstance(event, MessageAction) and event.source == EventSource.USER:
|
||||
likely_task = event
|
||||
elif isinstance(event, AgentFinishAction):
|
||||
# If a FinishAction is found, the user message after it is the one we just found (if any)
|
||||
break
|
||||
|
||||
# If a user message was found in the view after the last finish action, return it
|
||||
if likely_task is not None:
|
||||
return likely_task
|
||||
|
||||
# If no user message was found in the view after the last finish action,
|
||||
# it means either there were no user messages in the view, or the last event in the view was a FinishAction
|
||||
# In this case, we fall back to finding the very first user message in the full history.
|
||||
logger.warning(
|
||||
'No user message found in the view after the last FinishAction. Returning the first message in history.'
|
||||
)
|
||||
if self.history:
|
||||
# Look for the very first user message in the full history
|
||||
for event in self.history:
|
||||
if (
|
||||
isinstance(event, MessageAction)
|
||||
and event.source == EventSource.USER
|
||||
):
|
||||
return event
|
||||
|
||||
# If no user message is found in the entire history, raise an error
|
||||
raise ValueError('No user message found in history. This should not happen.')
|
||||
|
||||
def get_last_agent_message(self) -> MessageAction | None:
|
||||
for event in reversed(self.view):
|
||||
|
||||
@@ -6,11 +6,13 @@ from uuid import uuid4
|
||||
from prompt_toolkit.shortcuts import clear
|
||||
|
||||
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
|
||||
from openhands.cli.commands import (
|
||||
from openhands.controller import AgentController
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.cli_commands import (
|
||||
check_folder_security_agreement,
|
||||
handle_commands,
|
||||
)
|
||||
from openhands.cli.tui import (
|
||||
from openhands.core.cli_tui import (
|
||||
UsageMetrics,
|
||||
display_agent_running_message,
|
||||
display_banner,
|
||||
@@ -23,11 +25,9 @@ from openhands.cli.tui import (
|
||||
read_confirmation_input,
|
||||
read_prompt_input,
|
||||
)
|
||||
from openhands.cli.utils import (
|
||||
from openhands.core.cli_utils import (
|
||||
update_usage_metrics,
|
||||
)
|
||||
from openhands.controller import AgentController
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
parse_arguments,
|
||||
@@ -5,12 +5,12 @@ from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.shortcuts import clear, print_container
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
|
||||
from openhands.cli.settings import (
|
||||
from openhands.core.cli_settings import (
|
||||
display_settings,
|
||||
modify_llm_settings_advanced,
|
||||
modify_llm_settings_basic,
|
||||
)
|
||||
from openhands.cli.tui import (
|
||||
from openhands.core.cli_tui import (
|
||||
COLOR_GREY,
|
||||
UsageMetrics,
|
||||
cli_confirm,
|
||||
@@ -18,7 +18,7 @@ from openhands.cli.tui import (
|
||||
display_shutdown_message,
|
||||
display_status,
|
||||
)
|
||||
from openhands.cli.utils import (
|
||||
from openhands.core.cli_utils import (
|
||||
add_local_config_trusted_dir,
|
||||
get_local_config_trusted_dirs,
|
||||
read_file,
|
||||
@@ -5,19 +5,19 @@ from prompt_toolkit.shortcuts import print_container
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.cli.tui import (
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.cli_tui import (
|
||||
COLOR_GREY,
|
||||
UserCancelledError,
|
||||
cli_confirm,
|
||||
kb_cancel,
|
||||
)
|
||||
from openhands.cli.utils import (
|
||||
from openhands.core.cli_utils import (
|
||||
VERIFIED_ANTHROPIC_MODELS,
|
||||
VERIFIED_OPENAI_MODELS,
|
||||
VERIFIED_PROVIDERS,
|
||||
organize_models_and_providers,
|
||||
)
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.core.config.condenser_config import NoOpCondenserConfig
|
||||
from openhands.core.config.utils import OH_DEFAULT_AGENT
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
@@ -59,14 +58,12 @@ COMMANDS = {
|
||||
'/exit': 'Exit the application',
|
||||
'/help': 'Display available commands',
|
||||
'/init': 'Initialize a new repository',
|
||||
'/status': 'Display conversation details and usage metrics',
|
||||
'/new': 'Create a new conversation',
|
||||
'/status': 'Display session details and usage metrics',
|
||||
'/new': 'Create a new session',
|
||||
'/settings': 'Display and modify current settings',
|
||||
'/resume': 'Resume the agent when paused',
|
||||
}
|
||||
|
||||
print_lock = threading.Lock()
|
||||
|
||||
|
||||
class UsageMetrics:
|
||||
def __init__(self):
|
||||
@@ -139,7 +136,7 @@ def display_banner(session_id: str):
|
||||
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
|
||||
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML(f'<grey>Initialized conversation {session_id}</grey>'))
|
||||
print_formatted_text(HTML(f'<grey>Initialized session {session_id}</grey>'))
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
@@ -167,28 +164,28 @@ def display_initial_user_prompt(prompt: str):
|
||||
|
||||
# Prompt output display functions
|
||||
def display_event(event: Event, config: AppConfig) -> None:
|
||||
with print_lock:
|
||||
if isinstance(event, Action):
|
||||
if hasattr(event, 'thought'):
|
||||
display_message(event.thought)
|
||||
if isinstance(event, MessageAction):
|
||||
if event.source == EventSource.AGENT:
|
||||
display_message(event.content)
|
||||
if isinstance(event, CmdRunAction):
|
||||
display_command(event)
|
||||
if isinstance(event, CmdOutputObservation):
|
||||
display_command_output(event.content)
|
||||
if isinstance(event, FileEditAction):
|
||||
display_file_edit(event)
|
||||
if isinstance(event, FileEditObservation):
|
||||
display_file_edit(event)
|
||||
if isinstance(event, FileReadObservation):
|
||||
display_file_read(event)
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
display_agent_paused_message(event.agent_state)
|
||||
if isinstance(event, Action):
|
||||
if hasattr(event, 'thought'):
|
||||
display_message(event.thought)
|
||||
if isinstance(event, MessageAction):
|
||||
if event.source == EventSource.AGENT:
|
||||
display_message(event.content)
|
||||
if isinstance(event, CmdRunAction):
|
||||
display_command(event)
|
||||
if isinstance(event, CmdOutputObservation):
|
||||
display_command_output(event.content)
|
||||
if isinstance(event, FileEditAction):
|
||||
display_file_edit(event)
|
||||
if isinstance(event, FileEditObservation):
|
||||
display_file_edit(event)
|
||||
if isinstance(event, FileReadObservation):
|
||||
display_file_read(event)
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
display_agent_paused_message(event.agent_state)
|
||||
|
||||
|
||||
def display_message(message: str):
|
||||
time.sleep(0.2)
|
||||
message = message.strip()
|
||||
|
||||
if message:
|
||||
@@ -251,7 +248,6 @@ def display_file_edit(event: FileEditAction | FileEditObservation):
|
||||
title='File Edit',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_formatted_text('')
|
||||
print_container(container)
|
||||
|
||||
|
||||
@@ -266,7 +262,6 @@ def display_file_read(event: FileReadObservation):
|
||||
title='File Read',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_formatted_text('')
|
||||
print_container(container)
|
||||
|
||||
|
||||
@@ -379,13 +374,13 @@ def get_session_duration(session_init_time: float) -> str:
|
||||
def display_shutdown_message(usage_metrics: UsageMetrics, session_id: str):
|
||||
duration_str = get_session_duration(usage_metrics.session_init_time)
|
||||
|
||||
print_formatted_text(HTML('<grey>Closing current conversation...</grey>'))
|
||||
print_formatted_text(HTML('<grey>Closing current session...</grey>'))
|
||||
print_formatted_text('')
|
||||
display_usage_metrics(usage_metrics)
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML(f'<grey>Conversation duration: {duration_str}</grey>'))
|
||||
print_formatted_text(HTML(f'<grey>Session duration: {duration_str}</grey>'))
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML(f'<grey>Closed conversation {session_id}</grey>'))
|
||||
print_formatted_text(HTML(f'<grey>Closed session {session_id}</grey>'))
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
@@ -393,8 +388,8 @@ def display_status(usage_metrics: UsageMetrics, session_id: str):
|
||||
duration_str = get_session_duration(usage_metrics.session_init_time)
|
||||
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML(f'<grey>Conversation ID: {session_id}</grey>'))
|
||||
print_formatted_text(HTML(f'<grey>Uptime: {duration_str}</grey>'))
|
||||
print_formatted_text(HTML(f'<grey>Session ID: {session_id}</grey>'))
|
||||
print_formatted_text(HTML(f'<grey>Uptime: {duration_str}</grey>'))
|
||||
print_formatted_text('')
|
||||
display_usage_metrics(usage_metrics)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
import toml
|
||||
|
||||
from openhands.cli.tui import (
|
||||
from openhands.core.cli_tui import (
|
||||
UsageMetrics,
|
||||
)
|
||||
from openhands.events.event import Event
|
||||
from openhands.llm.metrics import Metrics
|
||||
|
||||
_LOCAL_CONFIG_FILE_PATH = Path.home() / '.openhands' / 'config.toml'
|
||||
_DEFAULT_CONFIG: dict[str, dict[str, list[str]]] = {'sandbox': {'trusted_dirs': []}}
|
||||
_DEFAULT_CONFIG: Dict[str, Dict[str, List[str]]] = {'sandbox': {'trusted_dirs': []}}
|
||||
|
||||
|
||||
def get_local_config_trusted_dirs() -> list[str]:
|
||||
@@ -39,8 +39,6 @@ class SandboxConfig(BaseModel):
|
||||
docker_runtime_kwargs: Additional keyword arguments to pass to the Docker runtime when running containers.
|
||||
This should be a JSON string that will be parsed into a dictionary.
|
||||
trusted_dirs: List of directories that can be trusted to run the OpenHands CLI.
|
||||
vscode_port: The port to use for VSCode. If None, a random port will be chosen.
|
||||
This is useful when deploying OpenHands in a remote machine where you need to expose a specific port.
|
||||
"""
|
||||
|
||||
remote_runtime_api_url: str | None = Field(default='http://localhost:8000')
|
||||
@@ -79,7 +77,6 @@ class SandboxConfig(BaseModel):
|
||||
docker_runtime_kwargs: dict | None = Field(default=None)
|
||||
selected_repo: str | None = Field(default=None)
|
||||
trusted_dirs: list[str] = Field(default_factory=list)
|
||||
vscode_port: int | None = Field(default=None)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import hashlib
|
||||
import os
|
||||
import uuid
|
||||
from typing import Callable
|
||||
from typing import Callable, Tuple, Type
|
||||
|
||||
from pydantic import SecretStr
|
||||
|
||||
@@ -126,8 +126,6 @@ def initialize_repository_for_runtime(
|
||||
)
|
||||
# Run setup script if it exists
|
||||
runtime.maybe_run_setup_script()
|
||||
# Set up git hooks if pre-commit.sh exists
|
||||
runtime.maybe_setup_git_hooks()
|
||||
|
||||
return repo_directory
|
||||
|
||||
@@ -173,7 +171,7 @@ def create_memory(
|
||||
|
||||
|
||||
def create_agent(config: AppConfig) -> Agent:
|
||||
agent_cls: type[Agent] = Agent.get_cls(config.default_agent)
|
||||
agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
|
||||
agent_config = config.get_agent_config(config.default_agent)
|
||||
llm_config = config.get_llm_config_from_agent(config.default_agent)
|
||||
|
||||
@@ -191,7 +189,7 @@ def create_controller(
|
||||
config: AppConfig,
|
||||
headless_mode: bool = True,
|
||||
replay_events: list[Event] | None = None,
|
||||
) -> tuple[AgentController, State | None]:
|
||||
) -> Tuple[AgentController, State | None]:
|
||||
event_stream = runtime.event_stream
|
||||
initial_state = None
|
||||
try:
|
||||
|
||||
@@ -86,6 +86,13 @@ class AgentRejectAction(Action):
|
||||
class AgentDelegateAction(Action):
|
||||
agent: str
|
||||
inputs: dict
|
||||
"""Deprecated.
|
||||
Delegate agents run similarly to the main agent:
|
||||
- start from a prompt (passed in the 'prompt' field)
|
||||
- end with an AgentFinishAction.
|
||||
"""
|
||||
prompt: str
|
||||
"""The prompt/task for the delegate agent"""
|
||||
thought: str = ''
|
||||
action: str = ActionType.DELEGATE
|
||||
|
||||
|
||||
@@ -10,13 +10,18 @@ class AgentDelegateObservation(Observation):
|
||||
|
||||
Attributes:
|
||||
content (str): The content of the observation.
|
||||
outputs (dict): The outputs of the delegated agent.
|
||||
outputs (dict): The outputs of the delegated agent. (deprecated)
|
||||
observation (str): The type of observation.
|
||||
"""
|
||||
|
||||
outputs: dict
|
||||
"""Deprecated.
|
||||
Delegate agents run similarly to the main agent:
|
||||
- start from a prompt (passed in the 'prompt' field)
|
||||
- end with an AgentFinishAction.
|
||||
"""
|
||||
observation: str = ObservationType.DELEGATE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return ''
|
||||
return self.content
|
||||
|
||||
@@ -6,14 +6,8 @@ from typing import Any
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.queries import (
|
||||
suggested_task_issue_graphql_query,
|
||||
suggested_task_pr_graphql_query,
|
||||
)
|
||||
from openhands.integrations.service_types import (
|
||||
BaseGitService,
|
||||
Branch,
|
||||
GitService,
|
||||
ProviderType,
|
||||
Repository,
|
||||
@@ -50,9 +44,6 @@ class GitHubService(BaseGitService, GitService):
|
||||
if base_domain:
|
||||
self.BASE_URL = f'https://{base_domain}/api/v3'
|
||||
|
||||
self.external_auth_id = external_auth_id
|
||||
self.external_auth_token = external_auth_token
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
return ProviderType.GITHUB.value
|
||||
@@ -293,23 +284,60 @@ class GitHubService(BaseGitService, GitService):
|
||||
Returns:
|
||||
- PRs authored by the user.
|
||||
- Issues assigned to the user.
|
||||
|
||||
Note: Queries are split to avoid timeout issues.
|
||||
"""
|
||||
# Get user info to use in queries
|
||||
user = await self.get_user()
|
||||
login = user.login
|
||||
tasks: list[SuggestedTask] = []
|
||||
|
||||
query = """
|
||||
query GetUserTasks($login: String!) {
|
||||
user(login: $login) {
|
||||
pullRequests(first: 100, states: [OPEN], orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
nodes {
|
||||
number
|
||||
title
|
||||
repository {
|
||||
nameWithOwner
|
||||
}
|
||||
mergeable
|
||||
commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
statusCheckRollup {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reviews(first: 100, states: [CHANGES_REQUESTED, COMMENTED]) {
|
||||
nodes {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
issues(first: 100, states: [OPEN], filterBy: {assignee: $login}, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
nodes {
|
||||
number
|
||||
title
|
||||
repository {
|
||||
nameWithOwner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {'login': login}
|
||||
|
||||
try:
|
||||
pr_response = await self.execute_graphql_query(
|
||||
suggested_task_pr_graphql_query, variables
|
||||
)
|
||||
pr_data = pr_response['data']['user']
|
||||
response = await self.execute_graphql_query(query, variables)
|
||||
data = response['data']['user']
|
||||
tasks: list[SuggestedTask] = []
|
||||
|
||||
# Process pull requests
|
||||
for pr in pr_data['pullRequests']['nodes']:
|
||||
for pr in data['pullRequests']['nodes']:
|
||||
repo_name = pr['repository']['nameWithOwner']
|
||||
|
||||
# Start with default task type
|
||||
@@ -345,24 +373,8 @@ class GitHubService(BaseGitService, GitService):
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
f'Error fetching suggested task for PRs: {e}',
|
||||
extra={
|
||||
'signal': 'github_suggested_tasks',
|
||||
'user_id': self.external_auth_id,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
# Execute issue query
|
||||
issue_response = await self.execute_graphql_query(
|
||||
suggested_task_issue_graphql_query, variables
|
||||
)
|
||||
issue_data = issue_response['data']['user']
|
||||
|
||||
# Process issues
|
||||
for issue in issue_data['issues']['nodes']:
|
||||
for issue in data['issues']['nodes']:
|
||||
repo_name = issue['repository']['nameWithOwner']
|
||||
tasks.append(
|
||||
SuggestedTask(
|
||||
@@ -375,17 +387,8 @@ class GitHubService(BaseGitService, GitService):
|
||||
)
|
||||
|
||||
return tasks
|
||||
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
f'Error fetching suggested task for issues: {e}',
|
||||
extra={
|
||||
'signal': 'github_suggested_tasks',
|
||||
'user_id': self.external_auth_id,
|
||||
},
|
||||
)
|
||||
|
||||
return tasks
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def get_repository_details_from_repo_name(
|
||||
self, repository: str
|
||||
@@ -401,52 +404,6 @@ class GitHubService(BaseGitService, GitService):
|
||||
is_public=not repo.get('private', True),
|
||||
)
|
||||
|
||||
async def get_branches(self, repository: str) -> list[Branch]:
|
||||
"""Get branches for a repository"""
|
||||
url = f'{self.BASE_URL}/repos/{repository}/branches'
|
||||
|
||||
# Set maximum branches to fetch (10 pages with 100 per page)
|
||||
MAX_BRANCHES = 1000
|
||||
PER_PAGE = 100
|
||||
|
||||
all_branches: list[Branch] = []
|
||||
page = 1
|
||||
|
||||
# Fetch up to 10 pages of branches
|
||||
while page <= 10 and len(all_branches) < MAX_BRANCHES:
|
||||
params = {'per_page': str(PER_PAGE), 'page': str(page)}
|
||||
response, headers = await self._make_request(url, params)
|
||||
|
||||
if not response: # No more branches
|
||||
break
|
||||
|
||||
for branch_data in response:
|
||||
# Extract the last commit date if available
|
||||
last_push_date = None
|
||||
if branch_data.get('commit') and branch_data['commit'].get('commit'):
|
||||
commit_info = branch_data['commit']['commit']
|
||||
if commit_info.get('committer') and commit_info['committer'].get(
|
||||
'date'
|
||||
):
|
||||
last_push_date = commit_info['committer']['date']
|
||||
|
||||
branch = Branch(
|
||||
name=branch_data.get('name'),
|
||||
commit_sha=branch_data.get('commit', {}).get('sha', ''),
|
||||
protected=branch_data.get('protected', False),
|
||||
last_push_date=last_push_date,
|
||||
)
|
||||
all_branches.append(branch)
|
||||
|
||||
page += 1
|
||||
|
||||
# Check if we've reached the last page
|
||||
link_header = headers.get('Link', '')
|
||||
if 'rel="next"' not in link_header:
|
||||
break
|
||||
|
||||
return all_branches
|
||||
|
||||
|
||||
github_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITHUB_SERVICE_CLS',
|
||||
|
||||
@@ -196,6 +196,7 @@ class GitLabService(BaseGitService, GitService):
|
||||
full_name=repo.get('path_with_namespace'),
|
||||
stargazers_count=repo.get('star_count'),
|
||||
git_provider=ProviderType.GITLAB,
|
||||
is_public=True,
|
||||
)
|
||||
for repo in response
|
||||
]
|
||||
|
||||
@@ -31,6 +31,7 @@ from openhands.server.types import AppMode
|
||||
class ProviderToken(BaseModel):
|
||||
token: SecretStr | None = Field(default=None)
|
||||
user_id: str | None = Field(default=None)
|
||||
host: str | None = Field(default=None)
|
||||
|
||||
model_config = {
|
||||
'frozen': True, # Makes the entire model immutable
|
||||
@@ -40,15 +41,20 @@ class ProviderToken(BaseModel):
|
||||
@classmethod
|
||||
def from_value(cls, token_value: ProviderToken | dict[str, str]) -> ProviderToken:
|
||||
"""Factory method to create a ProviderToken from various input types"""
|
||||
if isinstance(token_value, ProviderToken):
|
||||
if isinstance(token_value, cls):
|
||||
return token_value
|
||||
elif isinstance(token_value, dict):
|
||||
token_str = token_value.get('token')
|
||||
token_str = token_value.get('token', '')
|
||||
# Override with emtpy string if it was set to None
|
||||
# Cannot pass None to SecretStr
|
||||
if token_str is None:
|
||||
token_str = ''
|
||||
user_id = token_value.get('user_id')
|
||||
return cls(token=SecretStr(token_str), user_id=user_id)
|
||||
host = token_value.get('host')
|
||||
return cls(token=SecretStr(token_str), user_id=user_id, host=host)
|
||||
|
||||
else:
|
||||
raise ValueError('Unsupport Provider token type')
|
||||
raise ValueError('Unsupported Provider token type')
|
||||
|
||||
|
||||
PROVIDER_TOKEN_TYPE = MappingProxyType[ProviderType, ProviderToken]
|
||||
@@ -166,7 +172,8 @@ class ProviderHandler:
|
||||
query, per_page, sort, order
|
||||
)
|
||||
all_repos.extend(service_repos)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning(f'Error searching repos from {provider}: {e}')
|
||||
continue
|
||||
|
||||
return all_repos
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user