mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
85 Commits
feature/cl
...
chuck-buil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c556d6396 | ||
|
|
8bb5aa21b9 | ||
|
|
d3d70fcc60 | ||
|
|
7906eab6b1 | ||
|
|
547e1049f1 | ||
|
|
818cc60b52 | ||
|
|
431d2c1f43 | ||
|
|
07f23641a3 | ||
|
|
de84af5586 | ||
|
|
b7765ba3f7 | ||
|
|
b89f2e51e4 | ||
|
|
e09f93aa75 | ||
|
|
9f529b105a | ||
|
|
89e3d2a867 | ||
|
|
a7b9a4f291 | ||
|
|
88cd16ae21 | ||
|
|
a8a3e9e604 | ||
|
|
0061bcc0b0 | ||
|
|
9c9fa780b0 | ||
|
|
569ac16163 | ||
|
|
08096db29f | ||
|
|
b2b6ddf90c | ||
|
|
87fe36d811 | ||
|
|
39d255d313 | ||
|
|
e334b67f21 | ||
|
|
46f7738f41 | ||
|
|
3f3669dd34 | ||
|
|
cd65645eea | ||
|
|
8e88a7a277 | ||
|
|
b393d52439 | ||
|
|
faeec48365 | ||
|
|
d5c02bf87b | ||
|
|
14a4664fe8 | ||
|
|
774caf0607 | ||
|
|
3a7df33acf | ||
|
|
7222730df0 | ||
|
|
910177fc57 | ||
|
|
ac9badbd20 | ||
|
|
02c299d88f | ||
|
|
f65fbef649 | ||
|
|
3c2acad28d | ||
|
|
0f1780728e | ||
|
|
d3f3378a4c | ||
|
|
65f4164749 | ||
|
|
3f984d878b | ||
|
|
10b871f4ab | ||
|
|
d664f516db | ||
|
|
e74bbd81d1 | ||
|
|
ab893f93f0 | ||
|
|
5aba498e77 | ||
|
|
1523555eea | ||
|
|
30604c40fc | ||
|
|
8dc46b7206 | ||
|
|
69498bebb4 | ||
|
|
77ee9e25d9 | ||
|
|
74753036bb | ||
|
|
95d7c10608 | ||
|
|
c142cc27ff | ||
|
|
0e20fc206b | ||
|
|
e21475a88e | ||
|
|
921fec0019 | ||
|
|
049f839a62 | ||
|
|
0dde758e13 | ||
|
|
8257ae70cc | ||
|
|
4513bcc622 | ||
|
|
b5b9a3f40b | ||
|
|
8ea1259943 | ||
|
|
ddb2794adf | ||
|
|
79fdcad7ef | ||
|
|
1de70b8ce4 | ||
|
|
3baeecb27c | ||
|
|
69fddecc7f | ||
|
|
3afe5ccee5 | ||
|
|
3d5a8dcf5a | ||
|
|
2ee1abe22c | ||
|
|
148940f553 | ||
|
|
1f09296136 | ||
|
|
70e5d12ba9 | ||
|
|
bcb3160d95 | ||
|
|
174c691744 | ||
|
|
af34d446e9 | ||
|
|
6604924f76 | ||
|
|
b2def1e438 | ||
|
|
2b8e47aca9 | ||
|
|
dba8b28824 |
58
.github/workflows/cli-build-test.yml
vendored
58
.github/workflows/cli-build-test.yml
vendored
@@ -1,58 +0,0 @@
|
||||
# Workflow that builds and tests the CLI binary executable
|
||||
name: CLI - Build and Test Binary
|
||||
|
||||
# Run on pushes to main branch and all pull requests, but only when CLI files change
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "openhands-cli/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "openhands-cli/**"
|
||||
|
||||
# Cancel previous runs if a new commit is pushed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-test-binary:
|
||||
name: Build and test binary executable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: openhands-cli
|
||||
run: |
|
||||
uv sync
|
||||
|
||||
- name: Build binary executable
|
||||
working-directory: openhands-cli
|
||||
run: |
|
||||
./build.sh --install-pyinstaller | tee output.log
|
||||
echo "Full output:"
|
||||
cat output.log
|
||||
|
||||
if grep -q "❌" output.log; then
|
||||
echo "❌ Found failure marker in output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Build & test finished without ❌ markers"
|
||||
20
.github/workflows/lint.yml
vendored
20
.github/workflows/lint.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
npm run make-i18n && tsc
|
||||
npm run check-translation-completeness
|
||||
|
||||
# Run lint on the python code (excluding CLI and enterprise)
|
||||
# Run lint on the python code
|
||||
lint-python:
|
||||
name: Lint python
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
@@ -73,24 +73,6 @@ jobs:
|
||||
working-directory: ./enterprise
|
||||
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
lint-cli-python:
|
||||
name: Lint CLI python
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: "pip"
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit==3.7.0
|
||||
- name: Run pre-commit hooks
|
||||
working-directory: ./openhands-cli
|
||||
run: pre-commit run --all-files --config ../dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
# Check version consistency across documentation
|
||||
check-version-consistency:
|
||||
name: Check version consistency
|
||||
|
||||
30
.github/workflows/py-tests.yml
vendored
30
.github/workflows/py-tests.yml
vendored
@@ -104,33 +104,3 @@ jobs:
|
||||
- name: Run Unit Tests
|
||||
working-directory: ./enterprise
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./tests/unit
|
||||
|
||||
# Run CLI unit tests
|
||||
test-cli-python:
|
||||
name: CLI Unit Tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./openhands-cli
|
||||
run: |
|
||||
uv sync --group dev
|
||||
|
||||
- name: Run CLI unit tests
|
||||
working-directory: ./openhands-cli
|
||||
run: |
|
||||
uv run pytest -v
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,8 +31,7 @@ requirements.txt
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
# Note: openhands-cli.spec is intentionally tracked for CLI builds
|
||||
# *.spec
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
@@ -489,47 +489,6 @@ type = "noop"
|
||||
# Run the runtime sandbox container in privileged mode for use with docker-in-docker
|
||||
#privileged = false
|
||||
|
||||
#################################### MCP #####################################
|
||||
# Configuration for Model Context Protocol (MCP) servers
|
||||
# MCP allows OpenHands to communicate with external tool servers
|
||||
##############################################################################
|
||||
[mcp]
|
||||
# SSE servers - Server-Sent Events transport (legacy)
|
||||
#sse_servers = [
|
||||
# # Basic SSE server with just a URL
|
||||
# "http://localhost:8080/mcp/sse",
|
||||
#
|
||||
# # SSE server with authentication
|
||||
# {url = "https://api.example.com/mcp/sse", api_key = "your-api-key"}
|
||||
#]
|
||||
|
||||
# SHTTP servers - Streamable HTTP transport (recommended)
|
||||
#shttp_servers = [
|
||||
# # Basic SHTTP server with default 60s timeout
|
||||
# "https://api.example.com/mcp/shttp",
|
||||
#
|
||||
# # SHTTP server with custom timeout for long-running tools
|
||||
# {
|
||||
# url = "https://api.example.com/mcp/shttp",
|
||||
# api_key = "your-api-key",
|
||||
# timeout = 180 # 3 minutes for processing-heavy tools (1-3600 seconds)
|
||||
# }
|
||||
#]
|
||||
|
||||
# Stdio servers - Direct process communication (development only)
|
||||
#stdio_servers = [
|
||||
# # Basic stdio server
|
||||
# {name = "filesystem", command = "npx", args = ["@modelcontextprotocol/server-filesystem", "/"]},
|
||||
#
|
||||
# # Stdio server with environment variables
|
||||
# {
|
||||
# name = "fetch",
|
||||
# command = "uvx",
|
||||
# args = ["mcp-server-fetch"],
|
||||
# env = {DEBUG = "true"}
|
||||
# }
|
||||
#]
|
||||
|
||||
#################################### Model Routing ############################
|
||||
# Configuration for experimental model routing feature
|
||||
# Enables intelligent switching between different LLM models for specific purposes
|
||||
|
||||
@@ -3,9 +3,9 @@ repos:
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
|
||||
- id: check-yaml
|
||||
args: ["--allow-multiple-documents"]
|
||||
- id: debug-statements
|
||||
@@ -28,12 +28,12 @@ repos:
|
||||
entry: ruff check --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
args: [--fix, --unsafe-fixes]
|
||||
exclude: ^(third_party/|enterprise/|openhands-cli/)
|
||||
exclude: ^(third_party/|enterprise/)
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
entry: ruff format --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
exclude: ^(third_party/|enterprise/|openhands-cli/)
|
||||
exclude: ^(third_party/|enterprise/)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.15.0
|
||||
|
||||
@@ -67,19 +67,6 @@ sse_servers = [
|
||||
# External MCP service with authentication
|
||||
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
|
||||
]
|
||||
|
||||
# SHTTP Servers - Modern streamable HTTP transport (recommended)
|
||||
shttp_servers = [
|
||||
# Basic SHTTP server with default 60s timeout
|
||||
"https://api.example.com/mcp/shttp",
|
||||
|
||||
# Server with custom timeout for heavy operations
|
||||
{
|
||||
url = "https://files.example.com/mcp/shttp",
|
||||
api_key = "your-api-key",
|
||||
timeout = 1800 # 30 minutes for large file processing
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
@@ -131,17 +118,6 @@ SHTTP (Streamable HTTP) servers are configured using either a string URL or an o
|
||||
- Type: `str`
|
||||
- Description: API key for authentication
|
||||
|
||||
- `timeout` (optional)
|
||||
- Type: `int`
|
||||
- Default: `60`
|
||||
- Range: `1-3600` seconds (1 hour maximum)
|
||||
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
|
||||
- **Use Cases:**
|
||||
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries
|
||||
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls
|
||||
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations
|
||||
- **Note**: This timeout only applies to individual tool calls, not server connection establishment.
|
||||
|
||||
### Stdio Servers
|
||||
|
||||
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
|
||||
@@ -216,27 +192,5 @@ SHTTP is the modern HTTP-based transport protocol that provides enhanced feature
|
||||
|
||||
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
|
||||
|
||||
#### SHTTP Timeout Best Practices
|
||||
|
||||
When configuring SHTTP timeouts, consider these guidelines:
|
||||
|
||||
**Timeout Selection:**
|
||||
- **Database queries**: 30-60 seconds
|
||||
- **File operations**: 60-300 seconds (depending on file size)
|
||||
- **Web scraping**: 60-120 seconds
|
||||
- **Complex calculations**: 300-1800 seconds
|
||||
- **Batch processing**: 1800-3600 seconds (maximum)
|
||||
|
||||
**Error Handling:**
|
||||
When a tool call exceeds the configured timeout:
|
||||
- The operation is cancelled with an `asyncio.TimeoutError`
|
||||
- The agent receives a timeout error message
|
||||
- The server connection remains active for subsequent requests
|
||||
|
||||
**Monitoring:**
|
||||
- Set timeouts based on your tool's actual performance characteristics
|
||||
- Monitor timeout occurrences to optimize timeout values
|
||||
- Consider implementing server-side timeout handling for graceful degradation
|
||||
|
||||
### Standard Input/Output (stdio)
|
||||
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.
|
||||
|
||||
1
force_build.txt
Normal file
1
force_build.txt
Normal file
@@ -0,0 +1 @@
|
||||
test
|
||||
@@ -13,8 +13,7 @@ vi.mock("react-router", async () => {
|
||||
|
||||
vi.mock("#/context/conversation-context", () => ({
|
||||
useConversation: () => ({ conversationId: "test-conversation-id" }),
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) =>
|
||||
children,
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", async () => {
|
||||
@@ -30,18 +29,21 @@ vi.mock("react-i18next", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Zustand browser store
|
||||
// Mock redux
|
||||
const mockDispatch = vi.fn();
|
||||
let mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("#/stores/browser-store", () => ({
|
||||
useBrowserStore: () => mockBrowserState,
|
||||
}));
|
||||
vi.mock("react-redux", async () => {
|
||||
const actual = await vi.importActual("react-redux");
|
||||
return {
|
||||
...actual,
|
||||
useDispatch: () => mockDispatch,
|
||||
useSelector: () => mockBrowserState,
|
||||
};
|
||||
});
|
||||
|
||||
// Import the component after all mocks are set up
|
||||
import { BrowserPanel } from "#/components/features/browser/browser";
|
||||
@@ -53,9 +55,6 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -64,9 +63,6 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
@@ -79,11 +75,7 @@ describe("Browser", () => {
|
||||
// Set the mock state for this test
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
|
||||
@@ -12,7 +12,6 @@ import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
// Mock hooks
|
||||
const mockUseUserProviders = vi.fn();
|
||||
@@ -56,47 +55,20 @@ describe("MicroagentManagement", () => {
|
||||
]);
|
||||
|
||||
const renderMicroagentManagement = (config?: QueryClientConfig) =>
|
||||
renderWithProviders(<RouterStub />);
|
||||
|
||||
// Common test data
|
||||
const testRepository = {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github" as const,
|
||||
is_public: true,
|
||||
owner_type: "user" as const,
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
};
|
||||
|
||||
// Helper function to render with custom Zustand store state
|
||||
const renderWithCustomStore = (storeOverrides: Partial<any>) => {
|
||||
useMicroagentManagementStore.setState(storeOverrides);
|
||||
return renderWithProviders(<RouterStub />);
|
||||
};
|
||||
|
||||
// Helper function to render with update modal visible
|
||||
const renderWithUpdateModal = (additionalState: Partial<any> = {}) => {
|
||||
return renderWithCustomStore({
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: testRepository,
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to render with selected microagent
|
||||
const renderWithSelectedMicroagent = (
|
||||
microagent: any,
|
||||
additionalState: Partial<any> = {},
|
||||
) => {
|
||||
return renderWithCustomStore({
|
||||
selectedRepository: testRepository,
|
||||
selectedMicroagentItem: {
|
||||
microagent,
|
||||
conversation: null,
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem: null,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
@@ -209,23 +181,6 @@ describe("MicroagentManagement", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Reset Zustand store to default state
|
||||
useMicroagentManagementStore.setState({
|
||||
// Modal visibility states
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
|
||||
// Repository states
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
|
||||
// Microagent states
|
||||
selectedMicroagentItem: null,
|
||||
});
|
||||
|
||||
// Setup default hook mocks
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: ["github"],
|
||||
@@ -1387,10 +1342,28 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render modal when Zustand state is set to visible", async () => {
|
||||
// Render with modal already visible in Zustand state
|
||||
renderWithCustomStore({
|
||||
addMicroagentModalVisible: true,
|
||||
it("should render modal when Redux state is set to visible", async () => {
|
||||
// Render with modal already visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: true, // Start with modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that modal is rendered
|
||||
@@ -1660,16 +1633,29 @@ describe("MicroagentManagement", () => {
|
||||
pr_number: null,
|
||||
};
|
||||
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) => {
|
||||
// Set the store with the selected microagent item and a repository
|
||||
useMicroagentManagementStore.setState({
|
||||
selectedMicroagentItem,
|
||||
selectedRepository: testRepository,
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) =>
|
||||
renderWithProviders(<MicroagentManagementMain />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return renderWithProviders(<MicroagentManagementMain />);
|
||||
};
|
||||
|
||||
it("should render MicroagentManagementDefault when no microagent or conversation is selected", async () => {
|
||||
renderMicroagentManagementMain(null);
|
||||
|
||||
@@ -1994,8 +1980,31 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
it("should render update microagent modal when updateMicroagentModalVisible is true", async () => {
|
||||
// Render with update modal visible in Zustand state
|
||||
renderWithUpdateModal();
|
||||
// Render with update modal visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true, // Start with update modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that update modal is rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2006,7 +2015,30 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should display update microagent title when isUpdate is true", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the update title is displayed
|
||||
expect(
|
||||
@@ -2016,10 +2048,28 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should populate form fields with existing microagent data when updating", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithUpdateModal({
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: null,
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2036,7 +2086,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2064,7 +2137,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2087,7 +2183,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2113,7 +2232,27 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle update modal with empty microagent data", async () => {
|
||||
// Render with update modal visible but no microagent data
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that update modal is still rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2134,7 +2273,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for the content to be loaded and check that the form field is empty
|
||||
await waitFor(() => {
|
||||
@@ -2155,7 +2317,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithUpdateModal();
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the modal is rendered correctly
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2314,7 +2499,30 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should render learn something new button in microagent view", async () => {
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that the learn something new button is displayed
|
||||
expect(
|
||||
@@ -2326,7 +2534,30 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2355,7 +2586,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2387,7 +2641,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2417,7 +2694,30 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
|
||||
@@ -342,7 +342,13 @@ describe("InteractiveChatBox", () => {
|
||||
// Simulate parent component updating the value prop
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox onSubmit={onSubmit} onStop={onStop} />
|
||||
<InteractiveChatBox
|
||||
onSubmit={onSubmit}
|
||||
onStop={onStop}
|
||||
isWaitingForUserInput={true}
|
||||
hasSubstantiveAgentActions={true}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -60,7 +60,13 @@ describe("Check for hardcoded English strings", () => {
|
||||
test("InteractiveChatBox should not have hardcoded English strings", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox onSubmit={() => {}} onStop={() => {}} />
|
||||
<InteractiveChatBox
|
||||
onSubmit={() => {}}
|
||||
onStop={() => {}}
|
||||
isWaitingForUserInput={false}
|
||||
hasSubstantiveAgentActions={false}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
1734
frontend/package-lock.json
generated
1734
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,38 +7,38 @@
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.9.1",
|
||||
"@react-router/serve": "^7.9.1",
|
||||
"@react-router/node": "^7.8.2",
|
||||
"@react-router/serve": "^7.8.2",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@stripe/react-stripe-js": "^4.0.2",
|
||||
"@stripe/react-stripe-js": "^4.0.0",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tanstack/react-query": "^5.87.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "^9.0.10",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.19",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.30",
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"posthog-js": "^1.268.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -47,7 +47,8 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.9.1",
|
||||
"react-resizable-panels": "^3.0.5",
|
||||
"react-router": "^7.8.2",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
@@ -55,7 +56,7 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.1.4",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2",
|
||||
"zustand": "^5.0.8"
|
||||
@@ -97,16 +98,16 @@
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@react-router/dev": "^7.9.1",
|
||||
"@tailwindcss/typography": "^0.5.18",
|
||||
"@tanstack/eslint-plugin-query": "^5.90.1",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@react-router/dev": "^7.8.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.86.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -128,8 +129,8 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.0.0",
|
||||
"lint-staged": "^16.2.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.6",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.5.0",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.11.1'
|
||||
const PACKAGE_VERSION = '2.10.5'
|
||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { BrowserSnapshot } from "./browser-snapshot";
|
||||
import { EmptyBrowserMessage } from "./empty-browser-message";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
setUrl,
|
||||
setScreenshotSrc,
|
||||
} from "#/state/browser-slice";
|
||||
|
||||
export function BrowserPanel() {
|
||||
const { url, screenshotSrc, reset } = useBrowserStore();
|
||||
const { url, screenshotSrc } = useSelector(
|
||||
(state: RootState) => state.browser,
|
||||
);
|
||||
const { conversationId } = useConversationId();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
}, [conversationId, reset]);
|
||||
dispatch(setUrl(browserInitialState.url));
|
||||
dispatch(setScreenshotSrc(browserInitialState.screenshotSrc));
|
||||
}, [conversationId]);
|
||||
|
||||
const imgSrc =
|
||||
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
|
||||
|
||||
function getEntryPoint(
|
||||
@@ -46,7 +46,7 @@ function getEntryPoint(
|
||||
}
|
||||
|
||||
export function ChatInterface() {
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const { getErrorMessage } = useWSErrorMessage();
|
||||
const { send, isLoadingMessages, parsedEvents } = useWsClient();
|
||||
const { setOptimisticUserMessage, getOptimisticUserMessage } =
|
||||
@@ -141,7 +141,7 @@ export function ChatInterface() {
|
||||
|
||||
send(createChatMessage(prompt, imageUrls, uploadedFiles, timestamp));
|
||||
setOptimisticUserMessage(content);
|
||||
setMessageToSend("");
|
||||
dispatch(setMessageToSend(null));
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
@@ -156,6 +156,10 @@ export function ChatInterface() {
|
||||
setFeedbackPolarity(polarity);
|
||||
};
|
||||
|
||||
const isWaitingForUserInput =
|
||||
curAgentState === AgentState.AWAITING_USER_INPUT ||
|
||||
curAgentState === AgentState.FINISHED;
|
||||
|
||||
// Create a ScrollProvider with the scroll hook values
|
||||
const scrollProviderValue = {
|
||||
scrollRef,
|
||||
@@ -176,7 +180,9 @@ export function ChatInterface() {
|
||||
!optimisticUserMessage &&
|
||||
!userEventsExist && (
|
||||
<ChatSuggestions
|
||||
onSuggestionsClick={(message) => setMessageToSend(message)}
|
||||
onSuggestionsClick={(message) =>
|
||||
dispatch(setMessageToSend(message))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Note: We only hide chat suggestions when there's a user message */}
|
||||
@@ -231,6 +237,9 @@ export function ChatInterface() {
|
||||
<InteractiveChatBox
|
||||
onSubmit={handleSendMessage}
|
||||
onStop={handleStop}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={!!optimisticUserMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export function ChatMessage({
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className={cn(
|
||||
"rounded-xl relative w-fit max-w-full last:mb-4",
|
||||
"rounded-xl relative w-fit max-w-full",
|
||||
"flex flex-col gap-2",
|
||||
type === "user" && " p-4 bg-tertiary self-end",
|
||||
type === "agent" && "mt-6 max-w-full bg-transparent",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Suggestions } from "#/components/features/suggestions/suggestions";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface ChatSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -12,7 +13,9 @@ interface ChatSuggestionsProps {
|
||||
|
||||
export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { shouldHideSuggestions } = useConversationStore();
|
||||
const shouldHideSuggestions = useSelector(
|
||||
(state: RootState) => state.conversation.shouldHideSuggestions,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import {
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { useChatInputLogic } from "#/hooks/chat/use-chat-input-logic";
|
||||
import { useFileHandling } from "#/hooks/chat/use-file-handling";
|
||||
import { useGripResize } from "#/hooks/chat/use-grip-resize";
|
||||
@@ -8,7 +15,6 @@ import { useChatSubmission } from "#/hooks/chat/use-chat-submission";
|
||||
import { ChatInputGrip } from "./components/chat-input-grip";
|
||||
import { ChatInputContainer } from "./components/chat-input-container";
|
||||
import { HiddenFileInput } from "./components/hidden-file-input";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export interface CustomChatInputProps {
|
||||
disabled?: boolean;
|
||||
@@ -35,12 +41,10 @@ export function CustomChatInput({
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
}: CustomChatInputProps) {
|
||||
const {
|
||||
submittedMessage,
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
} = useConversationStore();
|
||||
const { submittedMessage } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Disable input when conversation is stopped
|
||||
const isConversationStopped = conversationStatus === "STOPPED";
|
||||
@@ -52,8 +56,8 @@ export function CustomChatInput({
|
||||
return;
|
||||
}
|
||||
onSubmit(submittedMessage);
|
||||
setSubmittedMessage(null);
|
||||
}, [submittedMessage, disabled, onSubmit, setSubmittedMessage]);
|
||||
dispatch(setSubmittedMessage(null));
|
||||
}, [submittedMessage, disabled, onSubmit, dispatch]);
|
||||
|
||||
// Custom hooks
|
||||
const {
|
||||
@@ -108,10 +112,10 @@ export function CustomChatInput({
|
||||
// Cleanup: reset suggestions visibility when component unmounts
|
||||
useEffect(
|
||||
() => () => {
|
||||
setShouldHideSuggestions(false);
|
||||
clearAllFiles();
|
||||
dispatch(setShouldHideSuggestions(false));
|
||||
dispatch(clearAllFiles());
|
||||
},
|
||||
[setShouldHideSuggestions, clearAllFiles],
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import {
|
||||
FileWriteAction,
|
||||
CommandAction,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { ErrorMessage } from "../error-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface ErrorEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ErrorEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: ErrorEventMessageProps) {
|
||||
if (!isErrorObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isFinishAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface FinishEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function FinishEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: FinishEventMessageProps) {
|
||||
if (!isFinishAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface GenericEventMessageWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function GenericEventMessageWrapper({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: GenericEventMessageWrapperProps) {
|
||||
return (
|
||||
<div>
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export { ErrorEventMessage } from "./error-event-message";
|
||||
export { UserAssistantEventMessage } from "./user-assistant-event-message";
|
||||
export { FinishEventMessage } from "./finish-event-message";
|
||||
export { RejectEventMessage } from "./reject-event-message";
|
||||
export { McpEventMessage } from "./mcp-event-message";
|
||||
export { TaskTrackingEventMessage } from "./task-tracking-event-message";
|
||||
export { ObservationPairEventMessage } from "./observation-pair-event-message";
|
||||
export { GenericEventMessageWrapper } from "./generic-event-message-wrapper";
|
||||
export { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
export { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { LikertScale } from "../../feedback/likert-scale";
|
||||
|
||||
interface LikertScaleWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function LikertScaleWrapper({
|
||||
event,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: LikertScaleWrapperProps) {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isMcpObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { MCPObservationContent } from "../mcp-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface McpEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function McpEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: McpEventMessageProps) {
|
||||
if (!isMcpObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "../microagent/microagent-status-indicator";
|
||||
|
||||
interface MicroagentStatusWrapperProps {
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function MicroagentStatusWrapper({
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: MicroagentStatusWrapperProps) {
|
||||
if (!microagentStatus || !actions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface ObservationPairEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function ObservationPairEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: ObservationPairEventMessageProps) {
|
||||
if (!isOpenHandsAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isRejectObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
|
||||
interface RejectEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
}
|
||||
|
||||
export function RejectEventMessage({ event }: RejectEventMessageProps) {
|
||||
if (!isRejectObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isTaskTrackingObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { TaskTrackingObservationContent } from "../task-tracking-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface TaskTrackingEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function TaskTrackingEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: TaskTrackingEventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isTaskTrackingObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { command } = event.extras;
|
||||
let title: React.ReactNode;
|
||||
let initiallyExpanded = false;
|
||||
|
||||
// Determine title and expansion state based on command
|
||||
if (command === "plan") {
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
|
||||
initiallyExpanded = true;
|
||||
} else {
|
||||
// command === "view"
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
|
||||
initiallyExpanded = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={<TaskTrackingObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
initiallyExpanded={initiallyExpanded}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isUserMessage, isAssistantMessage } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { ImageCarousel } from "../../images/image-carousel";
|
||||
import { FileList } from "../../files/file-list";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { parseMessageFromEvent } from "../event-content-helpers/parse-message-from-event";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface UserAssistantEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAssistantEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: UserAssistantEventMessageProps) {
|
||||
if (!isUserMessage(event) && !isAssistantMessage(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
{isAssistantMessage(event) && event.action === "message" && (
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,39 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import {
|
||||
isUserMessage,
|
||||
isErrorObservation,
|
||||
isAssistantMessage,
|
||||
isOpenHandsAction,
|
||||
isOpenHandsObservation,
|
||||
isFinishAction,
|
||||
isRejectObservation,
|
||||
isMcpObservation,
|
||||
isTaskTrackingObservation,
|
||||
} from "#/types/core/guards";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { ImageCarousel } from "../images/image-carousel";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { ErrorMessage } from "./error-message";
|
||||
import { MCPObservationContent } from "./mcp-observation-content";
|
||||
import { TaskTrackingObservationContent } from "./task-tracking-observation-content";
|
||||
import { getObservationResult } from "./event-content-helpers/get-observation-result";
|
||||
import { getEventContent } from "./event-content-helpers/get-event-content";
|
||||
import { GenericEventMessage } from "./generic-event-message";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "./microagent/microagent-status-indicator";
|
||||
import { FileList } from "../files/file-list";
|
||||
import { parseMessageFromEvent } from "./event-content-helpers/parse-message-from-event";
|
||||
import { LikertScale } from "../feedback/likert-scale";
|
||||
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useFeedbackExists } from "#/hooks/query/use-feedback-exists";
|
||||
import {
|
||||
ErrorEventMessage,
|
||||
UserAssistantEventMessage,
|
||||
FinishEventMessage,
|
||||
RejectEventMessage,
|
||||
McpEventMessage,
|
||||
TaskTrackingEventMessage,
|
||||
ObservationPairEventMessage,
|
||||
GenericEventMessageWrapper,
|
||||
} from "./event-message-components";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface EventMessageProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
@@ -41,7 +51,6 @@ interface EventMessageProps {
|
||||
isInLast10Actions: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
export function EventMessage({
|
||||
event,
|
||||
hasObservationPair,
|
||||
@@ -53,6 +62,7 @@ export function EventMessage({
|
||||
actions,
|
||||
isInLast10Actions,
|
||||
}: EventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShowConfirmationButtons =
|
||||
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
|
||||
|
||||
@@ -63,83 +73,194 @@ export function EventMessage({
|
||||
isLoading: isCheckingFeedback,
|
||||
} = useFeedbackExists(event.id);
|
||||
|
||||
// Common props for components that need them
|
||||
const commonProps = {
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
const renderLikertScale = () => {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Error observations
|
||||
if (isErrorObservation(event)) {
|
||||
return <ErrorEventMessage event={event} {...commonProps} />;
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Observation pairs with OpenHands actions
|
||||
if (hasObservationPair && isOpenHandsAction(event)) {
|
||||
return (
|
||||
<ObservationPairEventMessage
|
||||
event={event}
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return microagentStatus && actions ? (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
// Finish actions
|
||||
if (isFinishAction(event)) {
|
||||
return <FinishEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// User and assistant messages
|
||||
if (isUserMessage(event) || isAssistantMessage(event)) {
|
||||
return (
|
||||
<UserAssistantEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
{...commonProps}
|
||||
/>
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Reject observations
|
||||
if (isRejectObservation(event)) {
|
||||
return <RejectEventMessage event={event} />;
|
||||
if (isUserMessage(event) || isAssistantMessage(event)) {
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{isAssistantMessage(event) &&
|
||||
event.action === "message" &&
|
||||
renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isRejectObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// MCP observations
|
||||
if (isMcpObservation(event)) {
|
||||
return (
|
||||
<McpEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Task tracking observations
|
||||
if (isTaskTrackingObservation(event)) {
|
||||
const { command } = event.extras;
|
||||
let title: React.ReactNode;
|
||||
let initiallyExpanded = false;
|
||||
|
||||
// Determine title and expansion state based on command
|
||||
if (command === "plan") {
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
|
||||
initiallyExpanded = true;
|
||||
} else {
|
||||
// command === "view"
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
|
||||
initiallyExpanded = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<TaskTrackingEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={<TaskTrackingObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
initiallyExpanded={initiallyExpanded}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
return (
|
||||
<GenericEventMessageWrapper
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
<div>
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPrButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPrButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPrButtonProps) {
|
||||
@@ -22,7 +24,7 @@ export function GitControlBarPrButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
|
||||
const handlePrClick = () => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
|
||||
@@ -8,10 +8,12 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface GitControlBarPullButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBarPullButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
}: GitControlBarPullButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -20,7 +22,7 @@ export function GitControlBarPullButton({
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const hasRepository = conversation?.selected_repository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
|
||||
const handlePullClick = () => {
|
||||
posthog.capture("pull_button_clicked");
|
||||
|
||||
@@ -8,12 +8,14 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPushButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPushButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPushButtonProps) {
|
||||
@@ -22,7 +24,7 @@ export function GitControlBarPushButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
|
||||
const handlePushClick = () => {
|
||||
posthog.capture("push_button_clicked");
|
||||
|
||||
@@ -11,9 +11,17 @@ import { GitControlBarTooltipWrapper } from "./git-control-bar-tooltip-wrapper";
|
||||
|
||||
interface GitControlBarProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
export function GitControlBar({
|
||||
onSuggestionsClick,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: GitControlBarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: conversation } = useActiveConversation();
|
||||
@@ -22,6 +30,12 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
const gitProvider = conversation?.git_provider as Provider;
|
||||
const selectedBranch = conversation?.selected_branch;
|
||||
|
||||
// Button is enabled when the agent is waiting for user input, has substantive actions, and no optimistic message
|
||||
const isButtonEnabled =
|
||||
isWaitingForUserInput &&
|
||||
hasSubstantiveAgentActions &&
|
||||
!optimisticUserMessage;
|
||||
|
||||
const hasRepository = !!selectedRepository;
|
||||
|
||||
return (
|
||||
@@ -59,6 +73,7 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
>
|
||||
<GitControlBarPullButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
/>
|
||||
</GitControlBarTooltipWrapper>
|
||||
|
||||
@@ -69,6 +84,7 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
>
|
||||
<GitControlBarPushButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
@@ -81,6 +97,7 @@ export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
>
|
||||
<GitControlBarPrButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { isFileImage } from "#/utils/is-file-image";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { CustomChatInput } from "./custom-chat-input";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { GitControlBar } from "./git-control-bar";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import {
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} from "#/state/conversation-slice";
|
||||
import { processFiles, processImages } from "#/utils/file-processing";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface InteractiveChatBoxProps {
|
||||
onSubmit: (message: string, images: File[], files: File[]) => void;
|
||||
onStop: () => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function InteractiveChatBox({
|
||||
onSubmit,
|
||||
onStop,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: InteractiveChatBoxProps) {
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const curAgentState = useSelector(
|
||||
(state: RootState) => state.agent.curAgentState,
|
||||
);
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
// Helper function to validate and filter files
|
||||
@@ -52,24 +58,26 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to show loading indicators for files
|
||||
const showLoadingIndicators = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => addFileLoading(file.name));
|
||||
validImages.forEach((image) => addImageLoading(image.name));
|
||||
validFiles.forEach((file) => dispatch(addFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(addImageLoading(image.name)));
|
||||
};
|
||||
|
||||
// Helper function to handle successful file processing results
|
||||
const handleSuccessfulFiles = (fileResults: { successful: File[] }) => {
|
||||
if (fileResults.successful.length > 0) {
|
||||
addFiles(fileResults.successful);
|
||||
fileResults.successful.forEach((file) => removeFileLoading(file.name));
|
||||
dispatch(addFiles(fileResults.successful));
|
||||
fileResults.successful.forEach((file) =>
|
||||
dispatch(removeFileLoading(file.name)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to handle successful image processing results
|
||||
const handleSuccessfulImages = (imageResults: { successful: File[] }) => {
|
||||
if (imageResults.successful.length > 0) {
|
||||
addImages(imageResults.successful);
|
||||
dispatch(addImages(imageResults.successful));
|
||||
imageResults.successful.forEach((image) =>
|
||||
removeImageLoading(image.name),
|
||||
dispatch(removeImageLoading(image.name)),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -80,14 +88,14 @@ export function InteractiveChatBox({
|
||||
imageResults: { failed: { file: File; error: Error }[] },
|
||||
) => {
|
||||
fileResults.failed.forEach(({ file, error }) => {
|
||||
removeFileLoading(file.name);
|
||||
dispatch(removeFileLoading(file.name));
|
||||
displayErrorToast(
|
||||
`Failed to process file ${file.name}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
imageResults.failed.forEach(({ file, error }) => {
|
||||
removeImageLoading(file.name);
|
||||
dispatch(removeImageLoading(file.name));
|
||||
displayErrorToast(
|
||||
`Failed to process image ${file.name}: ${error.message}`,
|
||||
);
|
||||
@@ -96,8 +104,8 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to clear loading states on error
|
||||
const clearLoadingStates = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => removeFileLoading(file.name));
|
||||
validImages.forEach((image) => removeImageLoading(image.name));
|
||||
validFiles.forEach((file) => dispatch(removeFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(removeImageLoading(image.name)));
|
||||
};
|
||||
|
||||
const handleUpload = async (selectedFiles: File[]) => {
|
||||
@@ -132,7 +140,7 @@ export function InteractiveChatBox({
|
||||
|
||||
const handleSubmit = (message: string) => {
|
||||
onSubmit(message, images, files);
|
||||
clearAllFiles();
|
||||
dispatch(clearAllFiles());
|
||||
};
|
||||
|
||||
const handleSuggestionsClick = (suggestion: string) => {
|
||||
@@ -153,7 +161,12 @@ export function InteractiveChatBox({
|
||||
conversationStatus={conversation?.status || null}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<GitControlBar onSuggestionsClick={handleSuggestionsClick} />
|
||||
<GitControlBar
|
||||
onSuggestionsClick={handleSuggestionsClick}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={optimisticUserMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useHandleRuntimeActive } from "#/hooks/use-handle-runtime-active";
|
||||
import { LoadingMicroagentBody } from "./loading-microagent-body";
|
||||
import { LoadingMicroagentTextarea } from "./loading-microagent-textarea";
|
||||
import { useGetMicroagents } from "#/hooks/query/use-get-microagents";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface LaunchMicroagentModalProps {
|
||||
onClose: () => void;
|
||||
@@ -77,9 +76,9 @@ export function LaunchMicroagentModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Typography.Text className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
<span className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
{t("MICROAGENT$DEFINITION")}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
|
||||
<form
|
||||
data-testid="launch-microagent-modal"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
export function LoadingMicroagentBody() {
|
||||
const { t } = useTranslation();
|
||||
@@ -11,7 +10,7 @@ export function LoadingMicroagentBody() {
|
||||
{t("MICROAGENT$ADD_TO_MICROAGENT")}
|
||||
</h2>
|
||||
<Spinner size="lg" />
|
||||
<Typography.Text>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</Typography.Text>
|
||||
<p>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</p>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { SuccessIndicator } from "../success-indicator";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface MicroagentStatusIndicatorProps {
|
||||
status: MicroagentStatus;
|
||||
@@ -82,9 +81,7 @@ export function MicroagentStatusIndicator({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography.Text className="underline">{statusText}</Typography.Text>
|
||||
);
|
||||
return <span className="underline">{statusText}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { UploadedFile } from "./uploaded-file";
|
||||
import { UploadedImage } from "./uploaded-image";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { removeFile, removeImage } from "#/state/conversation-slice";
|
||||
|
||||
export function UploadedFiles() {
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
loadingFiles,
|
||||
loadingImages,
|
||||
removeFile,
|
||||
removeImage,
|
||||
} = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const loadingFiles = useSelector(
|
||||
(state: RootState) => state.conversation.loadingFiles,
|
||||
);
|
||||
const loadingImages = useSelector(
|
||||
(state: RootState) => state.conversation.loadingImages,
|
||||
);
|
||||
|
||||
const handleRemoveFile = (index: number) => {
|
||||
removeFile(index);
|
||||
dispatch(removeFile(index));
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
removeImage(index);
|
||||
dispatch(removeImage(index));
|
||||
};
|
||||
|
||||
// Don't render anything if there are no files, images, or loading items
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { RootState } from "#/store";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
@@ -12,7 +12,7 @@ import ClockIcon from "#/icons/u-clock-three.svg?react";
|
||||
import { ChatResumeAgentButton } from "../chat/chat-play-button";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { AgentLoading } from "./agent-loading";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setShouldShownAgentLoading } from "#/state/conversation-slice";
|
||||
import CircleErrorIcon from "#/icons/circle-error.svg?react";
|
||||
|
||||
export interface AgentStatusProps {
|
||||
@@ -29,7 +29,7 @@ export function AgentStatus({
|
||||
disabled = false,
|
||||
}: AgentStatusProps) {
|
||||
const { t } = useTranslation();
|
||||
const { setShouldShownAgentLoading } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curStatusMessage } = useStatusStore();
|
||||
const { webSocketStatus } = useWsClient();
|
||||
@@ -58,8 +58,8 @@ export function AgentStatus({
|
||||
|
||||
// Update global state when agent loading condition changes
|
||||
useEffect(() => {
|
||||
setShouldShownAgentLoading(shouldShownAgentLoading);
|
||||
}, [shouldShownAgentLoading, setShouldShownAgentLoading]);
|
||||
dispatch(setShouldShownAgentLoading(shouldShownAgentLoading));
|
||||
}, [shouldShownAgentLoading, dispatch]);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
getCreatePRPrompt,
|
||||
getCreateNewBranchPrompt,
|
||||
} from "#/utils/utils";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
|
||||
import ArrowUpIcon from "#/icons/u-arrow-up.svg?react";
|
||||
import ArrowDownIcon from "#/icons/u-arrow-down.svg?react";
|
||||
@@ -27,28 +28,28 @@ interface GitToolsSubmenuProps {
|
||||
|
||||
export function GitToolsSubmenu({ onClose }: GitToolsSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
const currentGitProvider = conversation?.git_provider as Provider;
|
||||
|
||||
const onGitPull = () => {
|
||||
setMessageToSend(getGitPullPrompt());
|
||||
dispatch(setMessageToSend(getGitPullPrompt()));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onGitPush = () => {
|
||||
setMessageToSend(getGitPushPrompt(currentGitProvider));
|
||||
dispatch(setMessageToSend(getGitPushPrompt(currentGitProvider)));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreatePR = () => {
|
||||
setMessageToSend(getCreatePRPrompt(currentGitProvider));
|
||||
dispatch(setMessageToSend(getCreatePRPrompt(currentGitProvider)));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreateNewBranch = () => {
|
||||
setMessageToSend(getCreateNewBranchPrompt());
|
||||
dispatch(setMessageToSend(getCreateNewBranchPrompt()));
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -8,7 +9,7 @@ import PrStatusIcon from "#/icons/pr-status.svg?react";
|
||||
import DocumentIcon from "#/icons/document.svg?react";
|
||||
import WaterIcon from "#/icons/u-water.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { REPO_SUGGESTIONS } from "#/utils/suggestions/repo-suggestions";
|
||||
import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants";
|
||||
|
||||
@@ -21,22 +22,22 @@ interface MacrosSubmenuProps {
|
||||
|
||||
export function MacrosSubmenu({ onClose }: MacrosSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onIncreaseTestCoverage = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE));
|
||||
onClose();
|
||||
};
|
||||
const onFixReadme = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.FIX_README);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.FIX_README));
|
||||
onClose();
|
||||
};
|
||||
const onAutoMergePRs = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS));
|
||||
onClose();
|
||||
};
|
||||
const onCleanDependencies = () => {
|
||||
setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES);
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES));
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ConversationPanelWrapper({
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-full w-full left-0 top-0 z-[9999] bg-black/80 rounded-xl",
|
||||
"absolute h-full w-full left-0 top-0 z-20 bg-black/80 rounded-xl",
|
||||
pathname === "/" && "bottom-0 top-0 md:top-3 md:bottom-3 h-auto",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ChatInterface } from "../../chat/chat-interface";
|
||||
|
||||
interface ChatInterfaceWrapperProps {
|
||||
@@ -8,16 +7,15 @@ interface ChatInterfaceWrapperProps {
|
||||
export function ChatInterfaceWrapper({
|
||||
isRightPanelShown,
|
||||
}: ChatInterfaceWrapperProps) {
|
||||
return (
|
||||
<div className="flex justify-center w-full h-full">
|
||||
<div
|
||||
className={cn(
|
||||
"w-full transition-all duration-300 ease-in-out",
|
||||
isRightPanelShown ? "max-w-4xl" : "max-w-6xl",
|
||||
)}
|
||||
>
|
||||
<ChatInterface />
|
||||
if (!isRightPanelShown) {
|
||||
return (
|
||||
<div className="flex justify-center w-full h-full">
|
||||
<div className="w-full max-w-[768px]">
|
||||
<ChatInterface />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return <ChatInterface />;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useWindowSize } from "@uidotdev/usehooks";
|
||||
import { RootState } from "#/store";
|
||||
import { MobileLayout } from "./mobile-layout";
|
||||
import { DesktopLayout } from "./desktop-layout";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export function ConversationMain() {
|
||||
const { width } = useWindowSize();
|
||||
const { isRightPanelShown } = useConversationStore();
|
||||
const isRightPanelShown = useSelector(
|
||||
(state: RootState) => state.conversation.isRightPanelShown,
|
||||
);
|
||||
|
||||
if (width && width <= 1024) {
|
||||
return <MobileLayout isRightPanelShown={isRightPanelShown} />;
|
||||
|
||||
@@ -1,64 +1,35 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { ChatInterfaceWrapper } from "./chat-interface-wrapper";
|
||||
import { ConversationTabContent } from "../conversation-tabs/conversation-tab-content/conversation-tab-content";
|
||||
import { ResizeHandle } from "../../../ui/resize-handle";
|
||||
import { useResizablePanels } from "#/hooks/use-resizable-panels";
|
||||
|
||||
interface DesktopLayoutProps {
|
||||
isRightPanelShown: boolean;
|
||||
}
|
||||
|
||||
export function DesktopLayout({ isRightPanelShown }: DesktopLayoutProps) {
|
||||
const { leftWidth, rightWidth, isDragging, containerRef, handleMouseDown } =
|
||||
useResizablePanels({
|
||||
defaultLeftWidth: 50,
|
||||
minLeftWidth: 30,
|
||||
maxLeftWidth: 80,
|
||||
storageKey: "desktop-layout-panel-width",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col overflow-hidden">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex flex-1 transition-all duration-300 ease-in-out overflow-hidden"
|
||||
style={{
|
||||
// Only apply smooth transitions when not dragging
|
||||
transitionProperty: isDragging ? "none" : "all",
|
||||
}}
|
||||
>
|
||||
{/* Left Panel (Chat) */}
|
||||
<div
|
||||
className="flex flex-col bg-base overflow-hidden transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
width: isRightPanelShown ? `${leftWidth}%` : "100%",
|
||||
transitionProperty: isDragging ? "none" : "all",
|
||||
}}
|
||||
>
|
||||
<ChatInterfaceWrapper isRightPanelShown={isRightPanelShown} />
|
||||
</div>
|
||||
|
||||
{/* Resize Handle */}
|
||||
{isRightPanelShown && <ResizeHandle onMouseDown={handleMouseDown} />}
|
||||
|
||||
{/* Right Panel */}
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-300 ease-in-out overflow-hidden",
|
||||
isRightPanelShown
|
||||
? "translate-x-0 opacity-100"
|
||||
: "w-0 translate-x-full opacity-0",
|
||||
)}
|
||||
style={{
|
||||
width: isRightPanelShown ? `${rightWidth}%` : "0%",
|
||||
transitionProperty: isDragging ? "opacity, transform" : "all",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-1 gap-3 min-w-max h-full">
|
||||
<ConversationTabContent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PanelGroup
|
||||
direction="horizontal"
|
||||
className="grow h-full min-h-0 min-w-0"
|
||||
autoSaveId="react-resizable-panels:layout"
|
||||
>
|
||||
<Panel minSize={30} maxSize={80} className="overflow-hidden bg-base">
|
||||
<ChatInterfaceWrapper isRightPanelShown={isRightPanelShown} />
|
||||
</Panel>
|
||||
{isRightPanelShown && (
|
||||
<>
|
||||
<PanelResizeHandle className="cursor-ew-resize" />
|
||||
<Panel
|
||||
minSize={20}
|
||||
maxSize={70}
|
||||
className="flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col flex-1 gap-3">
|
||||
<ConversationTabContent />
|
||||
</div>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,30 +8,20 @@ interface MobileLayoutProps {
|
||||
|
||||
export function MobileLayout({ isRightPanelShown }: MobileLayoutProps) {
|
||||
return (
|
||||
<div className="relative h-full flex flex-col overflow-hidden">
|
||||
{/* Chat area - shrinks when panel slides up */}
|
||||
<div className="flex flex-col gap-3 overflow-auto w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 bg-base overflow-hidden transition-all duration-300 ease-in-out",
|
||||
isRightPanelShown ? "flex-[0.6]" : "flex-1",
|
||||
"overflow-hidden w-full bg-base min-h-[600px]",
|
||||
!isRightPanelShown && "h-full",
|
||||
)}
|
||||
>
|
||||
<ChatInterface />
|
||||
</div>
|
||||
|
||||
{/* Bottom panel - slides up from bottom */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 left-0 right-0 transition-all duration-300 ease-in-out overflow-hidden",
|
||||
isRightPanelShown
|
||||
? "h-[40%] translate-y-0 opacity-100"
|
||||
: "h-0 translate-y-full opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="h-full flex flex-col gap-3 pt-2">
|
||||
{isRightPanelShown && (
|
||||
<div className="h-full w-full min-h-[494px] flex flex-col gap-3">
|
||||
<ConversationTabContent />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { lazy, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { ConversationLoading } from "../../conversation-loading";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { TabWrapper } from "./tab-wrapper";
|
||||
@@ -7,7 +9,6 @@ import { TabContainer } from "./tab-container";
|
||||
import { TabContentArea } from "./tab-content-area";
|
||||
import { ConversationTabTitle } from "../conversation-tab-title";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
// Lazy load all tab components
|
||||
const EditorTab = lazy(() => import("#/routes/changes-tab"));
|
||||
@@ -17,7 +18,12 @@ const ServedTab = lazy(() => import("#/routes/served-tab"));
|
||||
const VSCodeTab = lazy(() => import("#/routes/vscode-tab"));
|
||||
|
||||
export function ConversationTabContent() {
|
||||
const { selectedTab, shouldShownAgentLoading } = useConversationStore();
|
||||
const selectedTab = useSelector(
|
||||
(state: RootState) => state.conversation.selectedTab,
|
||||
);
|
||||
const { shouldShownAgentLoading } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import JupyterIcon from "#/icons/jupyter.svg?react";
|
||||
@@ -13,17 +14,21 @@ import { ChatActionTooltip } from "../../chat/chat-action-tooltip";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { VSCodeTooltipContent } from "./vscode-tooltip-content";
|
||||
import {
|
||||
useConversationStore,
|
||||
setHasRightPanelToggled,
|
||||
setSelectedTab,
|
||||
setIsRightPanelShown,
|
||||
type ConversationTab,
|
||||
} from "#/state/conversation-store";
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
export function ConversationTabs() {
|
||||
const {
|
||||
selectedTab,
|
||||
isRightPanelShown,
|
||||
setHasRightPanelToggled,
|
||||
setSelectedTab,
|
||||
} = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const selectedTab = useSelector(
|
||||
(state: RootState) => state.conversation.selectedTab,
|
||||
);
|
||||
const { isRightPanelShown } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
// Persist selectedTab and isRightPanelShown in localStorage
|
||||
const [persistedSelectedTab, setPersistedSelectedTab] =
|
||||
@@ -36,22 +41,18 @@ export function ConversationTabs() {
|
||||
useLocalStorage<boolean>("conversation-right-panel-shown", true);
|
||||
|
||||
const onTabChange = (value: ConversationTab | null) => {
|
||||
setSelectedTab(value);
|
||||
dispatch(setSelectedTab(value));
|
||||
// Persist the selected tab to localStorage
|
||||
setPersistedSelectedTab(value);
|
||||
};
|
||||
|
||||
// Initialize Zustand state from localStorage on component mount
|
||||
// Initialize Redux state from localStorage on component mount
|
||||
useEffect(() => {
|
||||
// Initialize selectedTab from localStorage if available
|
||||
setSelectedTab(persistedSelectedTab);
|
||||
setHasRightPanelToggled(persistedIsRightPanelShown);
|
||||
}, [
|
||||
setSelectedTab,
|
||||
setHasRightPanelToggled,
|
||||
persistedSelectedTab,
|
||||
persistedIsRightPanelShown,
|
||||
]);
|
||||
dispatch(setSelectedTab(persistedSelectedTab));
|
||||
dispatch(setIsRightPanelShown(persistedIsRightPanelShown));
|
||||
dispatch(setHasRightPanelToggled(persistedIsRightPanelShown));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePanelVisibilityChange = () => {
|
||||
@@ -71,13 +72,13 @@ export function ConversationTabs() {
|
||||
const onTabSelected = (tab: ConversationTab) => {
|
||||
if (selectedTab === tab && isRightPanelShown) {
|
||||
// If clicking the same active tab, close the drawer
|
||||
setHasRightPanelToggled(false);
|
||||
dispatch(setHasRightPanelToggled(false));
|
||||
setPersistedIsRightPanelShown(false);
|
||||
} else {
|
||||
// If clicking a different tab or drawer is closed, open drawer and select tab
|
||||
onTabChange(tab);
|
||||
if (!isRightPanelShown) {
|
||||
setHasRightPanelToggled(true);
|
||||
dispatch(setHasRightPanelToggled(true));
|
||||
setPersistedIsRightPanelShown(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setAddMicroagentModalVisible,
|
||||
setSelectedRepository,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
interface MicroagentManagementAddMicroagentButtonProps {
|
||||
@@ -12,16 +17,16 @@ export function MicroagentManagementAddMicroagentButton({
|
||||
}: MicroagentManagementAddMicroagentButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
addMicroagentModalVisible,
|
||||
setAddMicroagentModalVisible,
|
||||
setSelectedRepository,
|
||||
} = useMicroagentManagementStore();
|
||||
const { addMicroagentModalVisible } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
setAddMicroagentModalVisible(!addMicroagentModalVisible);
|
||||
setSelectedRepository(repository);
|
||||
dispatch(setAddMicroagentModalVisible(!addMicroagentModalVisible));
|
||||
dispatch(setSelectedRepository(repository));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { MicroagentManagementSidebar } from "./microagent-management-sidebar";
|
||||
import { MicroagentManagementMain } from "./microagent-management-main";
|
||||
import { MicroagentManagementUpsertMicroagentModal } from "./microagent-management-upsert-microagent-modal";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import {
|
||||
setAddMicroagentModalVisible,
|
||||
setUpdateMicroagentModalVisible,
|
||||
setLearnThisRepoModalVisible,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { useCreateConversationAndSubscribeMultiple } from "#/hooks/use-create-conversation-and-subscribe-multiple";
|
||||
import {
|
||||
LearnThisRepoFormData,
|
||||
@@ -100,15 +106,14 @@ export function MicroagentManagementContent() {
|
||||
updateMicroagentModalVisible,
|
||||
selectedRepository,
|
||||
learnThisRepoModalVisible,
|
||||
setAddMicroagentModalVisible,
|
||||
setUpdateMicroagentModalVisible,
|
||||
setLearnThisRepoModalVisible,
|
||||
} = useMicroagentManagementStore();
|
||||
} = useSelector((state: RootState) => state.microagentManagement);
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { createConversationAndSubscribe, isPending } =
|
||||
useCreateConversationAndSubscribeMultiple();
|
||||
|
||||
@@ -125,9 +130,9 @@ export function MicroagentManagementContent() {
|
||||
|
||||
const hideUpsertMicroagentModal = (isUpdate: boolean = false) => {
|
||||
if (isUpdate) {
|
||||
setUpdateMicroagentModalVisible(false);
|
||||
dispatch(setUpdateMicroagentModalVisible(false));
|
||||
} else {
|
||||
setAddMicroagentModalVisible(false);
|
||||
dispatch(setAddMicroagentModalVisible(false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,7 +264,7 @@ export function MicroagentManagementContent() {
|
||||
};
|
||||
|
||||
const hideLearnThisRepoModal = () => {
|
||||
setLearnThisRepoModalVisible(false);
|
||||
dispatch(setLearnThisRepoModalVisible(false));
|
||||
};
|
||||
|
||||
const handleLearnThisRepoConfirm = (formData: LearnThisRepoFormData) => {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementConversationStopped() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementError() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FaCircleInfo } from "react-icons/fa6";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import XIcon from "#/icons/x.svg?react";
|
||||
import { cn, getRepoMdCreatePrompt } from "#/utils/utils";
|
||||
import { LearnThisRepoFormData } from "#/types/microagent-management";
|
||||
@@ -25,7 +26,9 @@ export function MicroagentManagementLearnThisRepoModal({
|
||||
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
const { selectedRepository } = useMicroagentManagementStore();
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setLearnThisRepoModalVisible,
|
||||
setSelectedRepository,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
interface MicroagentManagementLearnThisRepoProps {
|
||||
@@ -10,13 +14,12 @@ interface MicroagentManagementLearnThisRepoProps {
|
||||
export function MicroagentManagementLearnThisRepo({
|
||||
repository,
|
||||
}: MicroagentManagementLearnThisRepoProps) {
|
||||
const { setLearnThisRepoModalVisible, setSelectedRepository } =
|
||||
useMicroagentManagementStore();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () => {
|
||||
setLearnThisRepoModalVisible(true);
|
||||
setSelectedRepository(repository);
|
||||
dispatch(setLearnThisRepoModalVisible(true));
|
||||
dispatch(setSelectedRepository(repository));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { MicroagentManagementDefault } from "./microagent-management-default";
|
||||
import { MicroagentManagementOpeningPr } from "./microagent-management-opening-pr";
|
||||
import { MicroagentManagementReviewPr } from "./microagent-management-review-pr";
|
||||
@@ -7,7 +8,9 @@ import { MicroagentManagementError } from "./microagent-management-error";
|
||||
import { MicroagentManagementConversationStopped } from "./microagent-management-conversation-stopped";
|
||||
|
||||
export function MicroagentManagementMain() {
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent, conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setSelectedMicroagentItem,
|
||||
setSelectedRepository,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
@@ -20,11 +25,11 @@ export function MicroagentManagementMicroagentCard({
|
||||
}: MicroagentManagementMicroagentCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
selectedMicroagentItem,
|
||||
setSelectedMicroagentItem,
|
||||
setSelectedRepository,
|
||||
} = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
status: conversationStatus,
|
||||
@@ -78,18 +83,20 @@ export function MicroagentManagementMicroagentCard({
|
||||
}, [microagent, conversation, selectedMicroagentItem]);
|
||||
|
||||
const onMicroagentCardClicked = () => {
|
||||
setSelectedMicroagentItem(
|
||||
microagent
|
||||
? {
|
||||
microagent,
|
||||
conversation: undefined,
|
||||
}
|
||||
: {
|
||||
microagent: undefined,
|
||||
conversation,
|
||||
},
|
||||
dispatch(
|
||||
setSelectedMicroagentItem(
|
||||
microagent
|
||||
? {
|
||||
microagent,
|
||||
conversation: null,
|
||||
}
|
||||
: {
|
||||
microagent: null,
|
||||
conversation,
|
||||
},
|
||||
),
|
||||
);
|
||||
setSelectedRepository(repository);
|
||||
dispatch(setSelectedRepository(repository));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementOpeningPr() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentManagementMicroagentCard } from "./microagent-management-microagent-card";
|
||||
import { MicroagentManagementLearnThisRepo } from "./microagent-management-learn-this-repo";
|
||||
import { useRepositoryMicroagents } from "#/hooks/query/use-repository-microagents";
|
||||
import { useMicroagentManagementConversations } from "#/hooks/query/use-microagent-management-conversations";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import { setSelectedMicroagentItem } from "#/state/microagent-management-slice";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
@@ -17,8 +19,11 @@ interface MicroagentManagementRepoMicroagentsProps {
|
||||
export function MicroagentManagementRepoMicroagents({
|
||||
repository,
|
||||
}: MicroagentManagementRepoMicroagentsProps) {
|
||||
const { selectedMicroagentItem, setSelectedMicroagentItem } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -55,22 +60,26 @@ export function MicroagentManagementRepoMicroagents({
|
||||
conversation.conversation_id === selectedConversation.conversation_id,
|
||||
);
|
||||
if (latestSelectedConversation) {
|
||||
setSelectedMicroagentItem({
|
||||
microagent: undefined,
|
||||
conversation: latestSelectedConversation,
|
||||
});
|
||||
dispatch(
|
||||
setSelectedMicroagentItem({
|
||||
microagent: null,
|
||||
conversation: latestSelectedConversation,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [conversations]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
setSelectedMicroagentItem({
|
||||
microagent: undefined,
|
||||
conversation: undefined,
|
||||
});
|
||||
dispatch(
|
||||
setSelectedMicroagentItem({
|
||||
microagent: null,
|
||||
conversation: null,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setSelectedMicroagentItem],
|
||||
[],
|
||||
);
|
||||
|
||||
// Show loading only when both queries are loading
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { getProviderName, constructPullRequestUrl } from "#/utils/utils";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
export function MicroagentManagementReviewPr() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedMicroagentItem } = useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { conversation } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Tab, Tabs } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { MicroagentManagementRepositories } from "./microagent-management-repositories";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface MicroagentManagementSidebarTabsProps {
|
||||
isSearchLoading?: boolean;
|
||||
@@ -14,7 +15,7 @@ export function MicroagentManagementSidebarTabs({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { repositories, personalRepositories, organizationRepositories } =
|
||||
useMicroagentManagementStore();
|
||||
useSelector((state: RootState) => state.microagentManagement);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentManagementSidebarHeader } from "./microagent-management-sidebar-header";
|
||||
@@ -6,7 +7,11 @@ import { MicroagentManagementSidebarTabs } from "./microagent-management-sidebar
|
||||
import { useGitRepositories } from "#/hooks/query/use-git-repositories";
|
||||
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
|
||||
import { GitProviderDropdown } from "#/components/features/home/git-provider-dropdown";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import {
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { cn } from "#/utils/utils";
|
||||
@@ -30,11 +35,7 @@ export function MicroagentManagementSidebar({
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300);
|
||||
|
||||
const {
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
} = useMicroagentManagementStore();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -95,9 +96,9 @@ export function MicroagentManagementSidebar({
|
||||
|
||||
useEffect(() => {
|
||||
if (!filteredRepositories?.length) {
|
||||
setPersonalRepositories([]);
|
||||
setOrganizationRepositories([]);
|
||||
setRepositories([]);
|
||||
dispatch(setPersonalRepositories([]));
|
||||
dispatch(setOrganizationRepositories([]));
|
||||
dispatch(setRepositories([]));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,16 +121,10 @@ export function MicroagentManagementSidebar({
|
||||
}
|
||||
});
|
||||
|
||||
setPersonalRepositories(personalRepos);
|
||||
setOrganizationRepositories(organizationRepos);
|
||||
setRepositories(otherRepos);
|
||||
}, [
|
||||
filteredRepositories,
|
||||
selectedProvider,
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
]);
|
||||
dispatch(setPersonalRepositories(personalRepos));
|
||||
dispatch(setOrganizationRepositories(organizationRepos));
|
||||
dispatch(setRepositories(otherRepos));
|
||||
}, [filteredRepositories, selectedProvider, dispatch]);
|
||||
|
||||
// Handle scroll to bottom for pagination
|
||||
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FaCircleInfo } from "react-icons/fa6";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import XIcon from "#/icons/x.svg?react";
|
||||
import { cn, extractRepositoryInfo } from "#/utils/utils";
|
||||
import { BadgeInput } from "#/components/shared/inputs/badge-input";
|
||||
@@ -31,8 +32,13 @@ export function MicroagentManagementUpsertMicroagentModal({
|
||||
const [triggers, setTriggers] = useState<string[]>([]);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
const { selectedRepository, selectedMicroagentItem } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
@@ -7,15 +8,20 @@ import { code } from "../markdown/code";
|
||||
import { ul, ol } from "../markdown/list";
|
||||
import { paragraph } from "../markdown/paragraph";
|
||||
import { anchor } from "../markdown/anchor";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { RootState } from "#/store";
|
||||
import { useRepositoryMicroagentContent } from "#/hooks/query/use-repository-microagent-content";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { extractRepositoryInfo } from "#/utils/utils";
|
||||
|
||||
export function MicroagentManagementViewMicroagentContent() {
|
||||
const { t } = useTranslation();
|
||||
const { selectedMicroagentItem, selectedRepository } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RootState } from "#/store";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { getProviderName, constructMicroagentUrl } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { setUpdateMicroagentModalVisible } from "#/state/microagent-management-slice";
|
||||
|
||||
export function MicroagentManagementViewMicroagentHeader() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
selectedMicroagentItem,
|
||||
selectedRepository,
|
||||
setUpdateMicroagentModalVisible,
|
||||
} = useMicroagentManagementStore();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
@@ -26,7 +32,7 @@ export function MicroagentManagementViewMicroagentHeader() {
|
||||
);
|
||||
|
||||
const handleLearnSomethingNew = () => {
|
||||
setUpdateMicroagentModalVisible(true);
|
||||
dispatch(setUpdateMicroagentModalVisible(true));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { MicroagentManagementViewMicroagentHeader } from "./microagent-management-view-microagent-header";
|
||||
import { MicroagentManagementViewMicroagentContent } from "./microagent-management-view-microagent-content";
|
||||
|
||||
export function MicroagentManagementViewMicroagent() {
|
||||
const { selectedMicroagentItem, selectedRepository } =
|
||||
useMicroagentManagementStore();
|
||||
const { selectedMicroagentItem } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.microagentManagement,
|
||||
);
|
||||
|
||||
const { microagent } = selectedMicroagentItem ?? {};
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -121,22 +120,6 @@ export function MCPServerForm({
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateTimeout = (timeoutStr: string): string | null => {
|
||||
if (!timeoutStr.trim()) return null; // Optional field
|
||||
|
||||
const timeout = parseInt(timeoutStr.trim(), 10);
|
||||
if (Number.isNaN(timeout)) {
|
||||
return t(I18nKey.SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER);
|
||||
}
|
||||
if (timeout <= 0) {
|
||||
return t(I18nKey.SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE);
|
||||
}
|
||||
if (timeout > 3600) {
|
||||
return t(I18nKey.SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateStdioServer = (formData: FormData): string | null => {
|
||||
const name = formData.get("name")?.toString().trim() || "";
|
||||
const command = formData.get("command")?.toString().trim() || "";
|
||||
@@ -165,14 +148,6 @@ export function MCPServerForm({
|
||||
if (urlError) return urlError;
|
||||
const urlDupError = validateUrlUniqueness(url);
|
||||
if (urlDupError) return urlDupError;
|
||||
|
||||
// Validate timeout for SHTTP servers only
|
||||
if (serverType === "shttp") {
|
||||
const timeoutStr = formData.get("timeout")?.toString() || "";
|
||||
const timeoutError = validateTimeout(timeoutStr);
|
||||
if (timeoutError) return timeoutError;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -228,23 +203,12 @@ export function MCPServerForm({
|
||||
if (serverType === "sse" || serverType === "shttp") {
|
||||
const url = formData.get("url")?.toString().trim();
|
||||
const apiKey = formData.get("api_key")?.toString().trim();
|
||||
const timeoutStr = formData.get("timeout")?.toString().trim();
|
||||
|
||||
const serverConfig: MCPServerConfig = {
|
||||
onSubmit({
|
||||
...baseConfig,
|
||||
url: url!,
|
||||
...(apiKey && { api_key: apiKey }),
|
||||
};
|
||||
|
||||
// Only add timeout for SHTTP servers
|
||||
if (serverType === "shttp" && timeoutStr) {
|
||||
const timeoutValue = parseInt(timeoutStr, 10);
|
||||
if (!Number.isNaN(timeoutValue)) {
|
||||
serverConfig.timeout = timeoutValue;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(serverConfig);
|
||||
});
|
||||
} else if (serverType === "stdio") {
|
||||
const name = formData.get("name")?.toString().trim();
|
||||
const command = formData.get("command")?.toString().trim();
|
||||
@@ -319,21 +283,6 @@ export function MCPServerForm({
|
||||
defaultValue={server?.api_key || ""}
|
||||
placeholder={t(I18nKey.SETTINGS$MCP_API_KEY_PLACEHOLDER)}
|
||||
/>
|
||||
|
||||
{serverType === "shttp" && (
|
||||
<SettingsInput
|
||||
testId="timeout-input"
|
||||
name="timeout"
|
||||
type="number"
|
||||
label="Timeout (seconds)"
|
||||
className="w-full max-w-[680px]"
|
||||
showOptionalTag
|
||||
defaultValue={server?.timeout?.toString() || ""}
|
||||
placeholder="60"
|
||||
min={1}
|
||||
max={3600}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
||||
@@ -8,7 +8,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
@@ -6,19 +7,19 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { ActionTooltip } from "../action-tooltip";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import { RiskAlert } from "#/components/shared/risk-alert";
|
||||
import WarningIcon from "#/icons/u-warning.svg?react";
|
||||
import { useEventMessageStore } from "#/stores/event-message-store";
|
||||
import { RootState } from "#/store";
|
||||
import { addSubmittedEventId } from "#/state/event-message-slice";
|
||||
|
||||
export function ConfirmationButtons() {
|
||||
const submittedEventIds = useEventMessageStore(
|
||||
(state) => state.submittedEventIds,
|
||||
);
|
||||
const addSubmittedEventId = useEventMessageStore(
|
||||
(state) => state.addSubmittedEventId,
|
||||
const submittedEventIds = useSelector(
|
||||
(state: RootState) => state.eventMessage.submittedEventIds,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { send, parsedEvents } = useWsClient();
|
||||
@@ -39,10 +40,10 @@ export function ConfirmationButtons() {
|
||||
return;
|
||||
}
|
||||
|
||||
addSubmittedEventId(awaitingAction.id);
|
||||
dispatch(addSubmittedEventId(awaitingAction.id));
|
||||
send(generateAgentStateChangeEvent(state));
|
||||
},
|
||||
[send, addSubmittedEventId],
|
||||
[send],
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ResizeHandleProps {
|
||||
onMouseDown: (e: React.MouseEvent) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ResizeHandle({ onMouseDown, className }: ResizeHandleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn("relative w-1 bg-transparent cursor-ew-resize", className)}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
{/* Visual indicator */}
|
||||
<div className="absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2" />
|
||||
|
||||
{/* Larger hit area for easier dragging */}
|
||||
<div className="absolute inset-y-0 -left-1 -right-1" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,33 +30,19 @@ export const useChatInputEvents = (
|
||||
ensureCursorVisible(chatInputRef.current);
|
||||
}, [smartResize, chatInputRef]);
|
||||
|
||||
// Handle paste events to clean up formatting and handle files
|
||||
// Handle paste events to clean up formatting
|
||||
const handlePaste = useCallback(
|
||||
(e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Check if there are files in the clipboard
|
||||
const files = Array.from(e.clipboardData.files);
|
||||
const hasFiles = files.length > 0;
|
||||
|
||||
if (hasFiles) {
|
||||
// Handle file paste - let the file handling system process the files
|
||||
// We'll trigger a custom event that the file handling system can listen to
|
||||
const customEvent = new CustomEvent("pasteFiles", {
|
||||
detail: { files },
|
||||
});
|
||||
document.dispatchEvent(customEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle text paste as before
|
||||
// Get plain text from clipboard
|
||||
const text = e.clipboardData.getData("text/plain");
|
||||
if (text) {
|
||||
// Insert plain text
|
||||
document.execCommand("insertText", false, text);
|
||||
// Trigger resize
|
||||
setTimeout(smartResize, 0);
|
||||
}
|
||||
|
||||
// Insert plain text
|
||||
document.execCommand("insertText", false, text);
|
||||
|
||||
// Trigger resize
|
||||
setTimeout(smartResize, 0);
|
||||
},
|
||||
[smartResize],
|
||||
);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useRef, useCallback, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
setMessageToSend,
|
||||
setIsRightPanelShown,
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
import {
|
||||
isContentEmpty,
|
||||
clearEmptyContent,
|
||||
getTextContent,
|
||||
} from "#/components/features/chat/utils/chat-input.utils";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
/**
|
||||
* Hook for managing chat input content logic
|
||||
@@ -12,21 +17,20 @@ import { useConversationStore } from "#/state/conversation-store";
|
||||
export const useChatInputLogic = () => {
|
||||
const chatInputRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
messageToSend,
|
||||
hasRightPanelToggled,
|
||||
setMessageToSend,
|
||||
setIsRightPanelShown,
|
||||
} = useConversationStore();
|
||||
const { messageToSend, hasRightPanelToggled } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Save current input value when drawer state changes
|
||||
useEffect(() => {
|
||||
if (chatInputRef.current) {
|
||||
const currentText = getTextContent(chatInputRef.current);
|
||||
setMessageToSend(currentText);
|
||||
setIsRightPanelShown(hasRightPanelToggled);
|
||||
dispatch(setMessageToSend(currentText));
|
||||
dispatch(setIsRightPanelShown(hasRightPanelToggled));
|
||||
}
|
||||
}, [hasRightPanelToggled, setMessageToSend, setIsRightPanelShown]);
|
||||
}, [hasRightPanelToggled, dispatch]);
|
||||
|
||||
// Helper function to check if contentEditable is truly empty
|
||||
const checkIsContentEmpty = useCallback(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback, useState, useEffect } from "react";
|
||||
import React, { useRef, useCallback, useState } from "react";
|
||||
|
||||
interface UseFileHandlingReturn {
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
@@ -31,25 +31,6 @@ export const useFileHandling = (
|
||||
[onFilesPaste],
|
||||
);
|
||||
|
||||
// Listen for paste events with files
|
||||
useEffect(() => {
|
||||
const handlePasteFiles = (event: CustomEvent) => {
|
||||
const files = event.detail.files as File[];
|
||||
if (files && files.length > 0) {
|
||||
addFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("pasteFiles", handlePasteFiles as EventListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"pasteFiles",
|
||||
handlePasteFiles as EventListener,
|
||||
);
|
||||
};
|
||||
}, [addFiles]);
|
||||
|
||||
// File icon click handler
|
||||
const handleFileIconClick = useCallback((isDisabled: boolean) => {
|
||||
if (!isDisabled && fileInputRef.current) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useAutoResize } from "#/hooks/use-auto-resize";
|
||||
import { CHAT_INPUT } from "#/utils/constants";
|
||||
import {
|
||||
IMessageToSend,
|
||||
useConversationStore,
|
||||
} from "#/state/conversation-store";
|
||||
setShouldHideSuggestions,
|
||||
} from "#/state/conversation-slice";
|
||||
import { CHAT_INPUT } from "#/utils/constants";
|
||||
|
||||
/**
|
||||
* Hook for managing grip resize functionality
|
||||
@@ -13,11 +14,11 @@ export const useGripResize = (
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>,
|
||||
messageToSend: IMessageToSend | null,
|
||||
) => {
|
||||
const gripRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [isGripVisible, setIsGripVisible] = useState(false);
|
||||
|
||||
const { setShouldHideSuggestions } = useConversationStore();
|
||||
|
||||
const gripRef = useRef<HTMLDivElement | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Drag state management callbacks
|
||||
const handleDragStart = useCallback(() => {
|
||||
@@ -47,9 +48,9 @@ export const useGripResize = (
|
||||
(height: number) => {
|
||||
// Hide suggestions when input height exceeds the threshold
|
||||
const shouldHideChatSuggestions = height > CHAT_INPUT.HEIGHT_THRESHOLD;
|
||||
setShouldHideSuggestions(shouldHideChatSuggestions);
|
||||
dispatch(setShouldHideSuggestions(shouldHideChatSuggestions));
|
||||
},
|
||||
[setShouldHideSuggestions],
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// Use the auto-resize hook with height change callback
|
||||
|
||||
@@ -10,7 +10,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -50,7 +49,6 @@ export function useAddMcpServer() {
|
||||
const shttpServer: MCPSHTTPServer = {
|
||||
url: server.url!,
|
||||
...(server.api_key && { api_key: server.api_key }),
|
||||
...(server.timeout !== undefined && { timeout: server.timeout }),
|
||||
};
|
||||
newConfig.shttp_servers.push(shttpServer);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -52,7 +51,6 @@ export function useUpdateMcpServer() {
|
||||
const shttpServer: MCPSHTTPServer = {
|
||||
url: server.url!,
|
||||
...(server.api_key && { api_key: server.api_key }),
|
||||
...(server.timeout !== undefined && { timeout: server.timeout }),
|
||||
};
|
||||
newConfig.shttp_servers[index] = shttpServer;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, RefObject } from "react";
|
||||
import { IMessageToSend } from "#/state/conversation-store";
|
||||
import { IMessageToSend } from "#/state/conversation-slice";
|
||||
import { useDragResize } from "./use-drag-resize";
|
||||
|
||||
// Constants
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
|
||||
interface UseResizablePanelsOptions {
|
||||
defaultLeftWidth?: number;
|
||||
minLeftWidth?: number;
|
||||
maxLeftWidth?: number;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
export function useResizablePanels({
|
||||
defaultLeftWidth = 50,
|
||||
minLeftWidth = 30,
|
||||
maxLeftWidth = 80,
|
||||
storageKey = "desktop-layout-panel-width",
|
||||
}: UseResizablePanelsOptions = {}) {
|
||||
const [persistedWidth, setPersistedWidth] = useLocalStorage<number>(
|
||||
storageKey,
|
||||
defaultLeftWidth,
|
||||
);
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState(persistedWidth);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const clampWidth = useCallback(
|
||||
(width: number) => Math.max(minLeftWidth, Math.min(maxLeftWidth, width)),
|
||||
[minLeftWidth, maxLeftWidth],
|
||||
);
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isDragging || !containerRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const mouseX = e.clientX - containerRect.left;
|
||||
const containerWidth = containerRect.width;
|
||||
const newLeftWidth = (mouseX / containerWidth) * 100;
|
||||
|
||||
const clampedWidth = clampWidth(newLeftWidth);
|
||||
setLeftWidth(clampedWidth);
|
||||
},
|
||||
[isDragging, clampWidth],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (isDragging) {
|
||||
setIsDragging(false);
|
||||
setPersistedWidth(leftWidth);
|
||||
}
|
||||
}, [isDragging, leftWidth, setPersistedWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "ew-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isDragging) {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
};
|
||||
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||
|
||||
const rightWidth = 100 - leftWidth;
|
||||
|
||||
return {
|
||||
leftWidth,
|
||||
rightWidth,
|
||||
isDragging,
|
||||
containerRef,
|
||||
handleMouseDown,
|
||||
};
|
||||
}
|
||||
@@ -806,9 +806,6 @@ export enum I18nKey {
|
||||
SETTINGS$MCP_ERROR_COMMAND_NO_SPACES = "SETTINGS$MCP_ERROR_COMMAND_NO_SPACES",
|
||||
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
|
||||
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
|
||||
SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER = "SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER",
|
||||
SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE = "SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE",
|
||||
SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED = "SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED",
|
||||
SETTINGS$MCP_SERVER_TYPE = "SETTINGS$MCP_SERVER_TYPE",
|
||||
SETTINGS$MCP_API_KEY_PLACEHOLDER = "SETTINGS$MCP_API_KEY_PLACEHOLDER",
|
||||
SETTINGS$MCP_COMMAND_ARGUMENTS = "SETTINGS$MCP_COMMAND_ARGUMENTS",
|
||||
|
||||
@@ -12895,54 +12895,6 @@
|
||||
"de": "Environment variables must follow KEY=value format",
|
||||
"uk": "Environment variables must follow KEY=value format"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER": {
|
||||
"en": "Timeout must be a valid number",
|
||||
"ja": "タイムアウトは有効な数値である必要があります",
|
||||
"zh-CN": "超时必须是有效数字",
|
||||
"zh-TW": "超時必須是有效數字",
|
||||
"ko-KR": "타임아웃은 유효한 숫자여야 합니다",
|
||||
"no": "Timeout må være et gyldig tall",
|
||||
"it": "Il timeout deve essere un numero valido",
|
||||
"pt": "O timeout deve ser um número válido",
|
||||
"es": "El timeout debe ser un número válido",
|
||||
"ar": "يجب أن يكون المهلة الزمنية رقمًا صالحًا",
|
||||
"fr": "Le timeout doit être un nombre valide",
|
||||
"tr": "Zaman aşımı geçerli bir sayı olmalıdır",
|
||||
"de": "Timeout muss eine gültige Zahl sein",
|
||||
"uk": "Таймаут повинен бути дійсним числом"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE": {
|
||||
"en": "Timeout must be positive",
|
||||
"ja": "タイムアウトは正の値である必要があります",
|
||||
"zh-CN": "超时必须为正数",
|
||||
"zh-TW": "超時必須為正數",
|
||||
"ko-KR": "타임아웃은 양수여야 합니다",
|
||||
"no": "Timeout må være positiv",
|
||||
"it": "Il timeout deve essere positivo",
|
||||
"pt": "O timeout deve ser positivo",
|
||||
"es": "El timeout debe ser positivo",
|
||||
"ar": "يجب أن تكون المهلة الزمنية موجبة",
|
||||
"fr": "Le timeout doit être positif",
|
||||
"tr": "Zaman aşımı pozitif olmalıdır",
|
||||
"de": "Timeout muss positiv sein",
|
||||
"uk": "Таймаут повинен бути позитивним"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED": {
|
||||
"en": "Timeout cannot exceed 3600 seconds (1 hour)",
|
||||
"ja": "タイムアウトは3600秒(1時間)を超えることはできません",
|
||||
"zh-CN": "超时不能超过3600秒(1小时)",
|
||||
"zh-TW": "超時不能超過3600秒(1小時)",
|
||||
"ko-KR": "타임아웃은 3600초(1시간)을 초과할 수 없습니다",
|
||||
"no": "Timeout kan ikke overstige 3600 sekunder (1 time)",
|
||||
"it": "Il timeout non può superare 3600 secondi (1 ora)",
|
||||
"pt": "O timeout não pode exceder 3600 segundos (1 hora)",
|
||||
"es": "El timeout no puede exceder 3600 segundos (1 hora)",
|
||||
"ar": "لا يمكن أن تتجاوز المهلة الزمنية 3600 ثانية (ساعة واحدة)",
|
||||
"fr": "Le timeout ne peut pas dépasser 3600 secondes (1 heure)",
|
||||
"tr": "Zaman aşımı 3600 saniyeyi (1 saat) aşamaz",
|
||||
"de": "Timeout kann 3600 Sekunden (1 Stunde) nicht überschreiten",
|
||||
"uk": "Таймаут не може перевищувати 3600 секунд (1 година)"
|
||||
},
|
||||
"SETTINGS$MCP_SERVER_TYPE": {
|
||||
"en": "Server Type",
|
||||
"ja": "サーバータイプ",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useEffectOnce } from "#/hooks/use-effect-once";
|
||||
import { clearJupyter } from "#/state/jupyter-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { resetConversationState } from "#/state/conversation-slice";
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
@@ -38,7 +38,6 @@ function AppContent() {
|
||||
const { mutate: startConversation } = useStartConversation();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const { providers } = useUserProviders();
|
||||
const { resetConversationState } = useConversationStore();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const clearTerminal = useCommandStore((state) => state.clearTerminal);
|
||||
@@ -87,14 +86,14 @@ function AppContent() {
|
||||
React.useEffect(() => {
|
||||
clearTerminal();
|
||||
dispatch(clearJupyter());
|
||||
resetConversationState();
|
||||
dispatch(resetConversationState());
|
||||
dispatch(setCurrentAgentState(AgentState.LOADING));
|
||||
}, [conversationId, clearTerminal, resetConversationState]);
|
||||
}, [conversationId, clearTerminal]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
clearTerminal();
|
||||
dispatch(clearJupyter());
|
||||
resetConversationState();
|
||||
dispatch(resetConversationState());
|
||||
dispatch(setCurrentAgentState(AgentState.LOADING));
|
||||
});
|
||||
|
||||
@@ -111,7 +110,9 @@ function AppContent() {
|
||||
<ConversationTabs />
|
||||
</div>
|
||||
|
||||
<ConversationMain />
|
||||
<div className="flex h-full overflow-auto">
|
||||
<ConversationMain />
|
||||
</div>
|
||||
</div>
|
||||
</EventHandler>
|
||||
</ConversationSubscriptionsProvider>
|
||||
|
||||
@@ -20,7 +20,6 @@ interface MCPServerConfig {
|
||||
name?: string;
|
||||
url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -68,7 +67,6 @@ function MCPSettingsScreen() {
|
||||
type: "shttp" as const,
|
||||
url: typeof server === "string" ? server : server.url,
|
||||
api_key: typeof server === "object" ? server.api_key : undefined,
|
||||
timeout: typeof server === "object" ? server.timeout : undefined,
|
||||
})),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { trackError } from "#/utils/error-handler";
|
||||
import { appendSecurityAnalyzerInput } from "#/state/security-analyzer-slice";
|
||||
import useMetricsStore from "#/stores/metrics-store";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
import store from "#/store";
|
||||
@@ -12,10 +13,6 @@ import { handleObservationMessage } from "./observations";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { appendJupyterInput } from "#/state/jupyter-slice";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import {
|
||||
ActionSecurityRisk,
|
||||
useSecurityAnalyzerStore,
|
||||
} from "#/stores/security-analyzer-store";
|
||||
|
||||
export function handleActionMessage(message: ActionMessage) {
|
||||
if (message.args?.hidden) {
|
||||
@@ -41,22 +38,7 @@ export function handleActionMessage(message: ActionMessage) {
|
||||
}
|
||||
|
||||
if ("args" in message && "security_risk" in message.args) {
|
||||
useSecurityAnalyzerStore.getState().appendSecurityAnalyzerInput({
|
||||
id: message.id,
|
||||
args: {
|
||||
command: message.args.command,
|
||||
code: message.args.code,
|
||||
content: message.args.content,
|
||||
security_risk: message.args
|
||||
.security_risk as unknown as ActionSecurityRisk,
|
||||
confirmation_state: message.args.confirmation_state as
|
||||
| "awaiting_confirmation"
|
||||
| "confirmed"
|
||||
| "rejected"
|
||||
| undefined,
|
||||
},
|
||||
message: message.message,
|
||||
});
|
||||
store.dispatch(appendSecurityAnalyzerInput(message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { setUrl, setScreenshotSrc } from "#/state/browser-slice";
|
||||
import store from "#/store";
|
||||
import { ObservationMessage } from "#/types/message";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { appendJupyterOutput } from "#/state/jupyter-slice";
|
||||
import ObservationType from "#/types/observation-type";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
|
||||
export function handleObservationMessage(message: ObservationMessage) {
|
||||
switch (message.observation) {
|
||||
@@ -34,14 +34,11 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
break;
|
||||
case ObservationType.BROWSE:
|
||||
case ObservationType.BROWSE_INTERACTIVE:
|
||||
if (
|
||||
message.extras?.screenshot &&
|
||||
typeof message.extras.screenshot === "string"
|
||||
) {
|
||||
useBrowserStore.getState().setScreenshotSrc(message.extras.screenshot);
|
||||
if (message.extras?.screenshot) {
|
||||
store.dispatch(setScreenshotSrc(message.extras?.screenshot));
|
||||
}
|
||||
if (message.extras?.url && typeof message.extras.url === "string") {
|
||||
useBrowserStore.getState().setUrl(message.extras.url);
|
||||
if (message.extras?.url) {
|
||||
store.dispatch(setUrl(message.extras.url));
|
||||
}
|
||||
break;
|
||||
case ObservationType.AGENT_STATE_CHANGED:
|
||||
@@ -66,29 +63,19 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
|
||||
switch (observation) {
|
||||
case "browse":
|
||||
if (
|
||||
message.extras?.screenshot &&
|
||||
typeof message.extras.screenshot === "string"
|
||||
) {
|
||||
useBrowserStore
|
||||
.getState()
|
||||
.setScreenshotSrc(message.extras.screenshot);
|
||||
if (message.extras?.screenshot) {
|
||||
store.dispatch(setScreenshotSrc(message.extras.screenshot));
|
||||
}
|
||||
if (message.extras?.url && typeof message.extras.url === "string") {
|
||||
useBrowserStore.getState().setUrl(message.extras.url);
|
||||
if (message.extras?.url) {
|
||||
store.dispatch(setUrl(message.extras.url));
|
||||
}
|
||||
break;
|
||||
case "browse_interactive":
|
||||
if (
|
||||
message.extras?.screenshot &&
|
||||
typeof message.extras.screenshot === "string"
|
||||
) {
|
||||
useBrowserStore
|
||||
.getState()
|
||||
.setScreenshotSrc(message.extras.screenshot);
|
||||
if (message.extras?.screenshot) {
|
||||
store.dispatch(setScreenshotSrc(message.extras.screenshot));
|
||||
}
|
||||
if (message.extras?.url && typeof message.extras.url === "string") {
|
||||
useBrowserStore.getState().setUrl(message.extras.url);
|
||||
if (message.extras?.url) {
|
||||
store.dispatch(setUrl(message.extras.url));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
25
frontend/src/state/browser-slice.ts
Normal file
25
frontend/src/state/browser-slice.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export const initialState = {
|
||||
// URL of browser window (placeholder for now, will be replaced with the actual URL later)
|
||||
url: "https://github.com/All-Hands-AI/OpenHands",
|
||||
// Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later)
|
||||
screenshotSrc: "",
|
||||
};
|
||||
|
||||
export const browserSlice = createSlice({
|
||||
name: "browser",
|
||||
initialState,
|
||||
reducers: {
|
||||
setUrl: (state, action) => {
|
||||
state.url = action.payload;
|
||||
},
|
||||
setScreenshotSrc: (state, action) => {
|
||||
state.screenshotSrc = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setUrl, setScreenshotSrc } = browserSlice.actions;
|
||||
|
||||
export default browserSlice.reducer;
|
||||
151
frontend/src/state/conversation-slice.tsx
Normal file
151
frontend/src/state/conversation-slice.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export type ConversationTab =
|
||||
| "editor"
|
||||
| "browser"
|
||||
| "jupyter"
|
||||
| "served"
|
||||
| "vscode"
|
||||
| "terminal";
|
||||
|
||||
export interface IMessageToSend {
|
||||
text: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ConversationState {
|
||||
isRightPanelShown: boolean;
|
||||
selectedTab: ConversationTab | null;
|
||||
images: File[];
|
||||
files: File[];
|
||||
loadingFiles: string[]; // File names currently being processed
|
||||
loadingImages: string[]; // Image names currently being processed
|
||||
messageToSend: IMessageToSend | null;
|
||||
shouldShownAgentLoading: boolean;
|
||||
submittedMessage: string | null;
|
||||
shouldHideSuggestions: boolean; // New state to hide suggestions when input expands
|
||||
hasRightPanelToggled: boolean;
|
||||
}
|
||||
|
||||
export const conversationSlice = createSlice({
|
||||
name: "conversation",
|
||||
initialState: {
|
||||
isRightPanelShown: true,
|
||||
selectedTab: "editor" as ConversationTab,
|
||||
shouldStopConversation: false,
|
||||
shouldStartConversation: false,
|
||||
images: [],
|
||||
files: [],
|
||||
loadingFiles: [],
|
||||
loadingImages: [],
|
||||
messageToSend: null,
|
||||
shouldShownAgentLoading: false,
|
||||
submittedMessage: null,
|
||||
shouldHideSuggestions: false, // Initialize to false
|
||||
hasRightPanelToggled: true,
|
||||
} as ConversationState,
|
||||
reducers: {
|
||||
setIsRightPanelShown: (state, action) => {
|
||||
state.isRightPanelShown = action.payload;
|
||||
},
|
||||
setSelectedTab: (state, action) => {
|
||||
state.selectedTab = action.payload;
|
||||
},
|
||||
setShouldShownAgentLoading: (state, action) => {
|
||||
state.shouldShownAgentLoading = action.payload;
|
||||
},
|
||||
setShouldHideSuggestions: (state, action) => {
|
||||
state.shouldHideSuggestions = action.payload;
|
||||
},
|
||||
addImages: (state, action) => {
|
||||
state.images = [...state.images, ...action.payload];
|
||||
},
|
||||
addFiles: (state, action) => {
|
||||
state.files = [...state.files, ...action.payload];
|
||||
},
|
||||
removeImage: (state, action) => {
|
||||
state.images.splice(action.payload, 1);
|
||||
},
|
||||
removeFile: (state, action) => {
|
||||
state.files.splice(action.payload, 1);
|
||||
},
|
||||
clearImages: (state) => {
|
||||
state.images = [];
|
||||
},
|
||||
clearFiles: (state) => {
|
||||
state.files = [];
|
||||
},
|
||||
clearAllFiles: (state) => {
|
||||
state.images = [];
|
||||
state.files = [];
|
||||
state.loadingFiles = [];
|
||||
state.loadingImages = [];
|
||||
},
|
||||
// Loading state management
|
||||
addFileLoading: (state, action) => {
|
||||
if (!state.loadingFiles.includes(action.payload)) {
|
||||
state.loadingFiles.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeFileLoading: (state, action) => {
|
||||
state.loadingFiles = state.loadingFiles.filter(
|
||||
(name) => name !== action.payload,
|
||||
);
|
||||
},
|
||||
addImageLoading: (state, action) => {
|
||||
if (!state.loadingImages.includes(action.payload)) {
|
||||
state.loadingImages.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeImageLoading: (state, action) => {
|
||||
state.loadingImages = state.loadingImages.filter(
|
||||
(name) => name !== action.payload,
|
||||
);
|
||||
},
|
||||
clearAllLoading: (state) => {
|
||||
state.loadingFiles = [];
|
||||
state.loadingImages = [];
|
||||
},
|
||||
setMessageToSend: (state, action) => {
|
||||
state.messageToSend = {
|
||||
text: action.payload,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
},
|
||||
setSubmittedMessage: (state, action) => {
|
||||
state.submittedMessage = action.payload;
|
||||
},
|
||||
// Reset conversation state (useful for cleanup)
|
||||
resetConversationState: (state) => {
|
||||
state.shouldHideSuggestions = false;
|
||||
},
|
||||
setHasRightPanelToggled: (state, action) => {
|
||||
state.hasRightPanelToggled = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setIsRightPanelShown,
|
||||
setSelectedTab,
|
||||
setShouldShownAgentLoading,
|
||||
setShouldHideSuggestions,
|
||||
addImages,
|
||||
addFiles,
|
||||
removeImage,
|
||||
removeFile,
|
||||
clearImages,
|
||||
clearFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
clearAllLoading,
|
||||
setMessageToSend,
|
||||
setSubmittedMessage,
|
||||
resetConversationState,
|
||||
setHasRightPanelToggled,
|
||||
} = conversationSlice.actions;
|
||||
|
||||
export default conversationSlice.reducer;
|
||||
@@ -1,217 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
export type ConversationTab =
|
||||
| "editor"
|
||||
| "browser"
|
||||
| "jupyter"
|
||||
| "served"
|
||||
| "vscode"
|
||||
| "terminal";
|
||||
|
||||
export interface IMessageToSend {
|
||||
text: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ConversationState {
|
||||
isRightPanelShown: boolean;
|
||||
selectedTab: ConversationTab | null;
|
||||
images: File[];
|
||||
files: File[];
|
||||
loadingFiles: string[]; // File names currently being processed
|
||||
loadingImages: string[]; // Image names currently being processed
|
||||
messageToSend: IMessageToSend | null;
|
||||
shouldShownAgentLoading: boolean;
|
||||
submittedMessage: string | null;
|
||||
shouldHideSuggestions: boolean; // New state to hide suggestions when input expands
|
||||
hasRightPanelToggled: boolean;
|
||||
}
|
||||
|
||||
interface ConversationActions {
|
||||
setIsRightPanelShown: (isRightPanelShown: boolean) => void;
|
||||
setSelectedTab: (selectedTab: ConversationTab | null) => void;
|
||||
setShouldShownAgentLoading: (shouldShownAgentLoading: boolean) => void;
|
||||
setShouldHideSuggestions: (shouldHideSuggestions: boolean) => void;
|
||||
addImages: (images: File[]) => void;
|
||||
addFiles: (files: File[]) => void;
|
||||
removeImage: (index: number) => void;
|
||||
removeFile: (index: number) => void;
|
||||
clearImages: () => void;
|
||||
clearFiles: () => void;
|
||||
clearAllFiles: () => void;
|
||||
addFileLoading: (fileName: string) => void;
|
||||
removeFileLoading: (fileName: string) => void;
|
||||
addImageLoading: (imageName: string) => void;
|
||||
removeImageLoading: (imageName: string) => void;
|
||||
clearAllLoading: () => void;
|
||||
setMessageToSend: (text: string) => void;
|
||||
setSubmittedMessage: (message: string | null) => void;
|
||||
resetConversationState: () => void;
|
||||
setHasRightPanelToggled: (hasRightPanelToggled: boolean) => void;
|
||||
}
|
||||
|
||||
type ConversationStore = ConversationState & ConversationActions;
|
||||
|
||||
// Helper function to get initial right panel state from localStorage
|
||||
const getInitialRightPanelState = (): boolean => {
|
||||
const stored = localStorage.getItem("conversation-right-panel-shown");
|
||||
return stored !== null ? JSON.parse(stored) : true;
|
||||
};
|
||||
|
||||
export const useConversationStore = create<ConversationStore>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
// Initial state
|
||||
isRightPanelShown: getInitialRightPanelState(),
|
||||
selectedTab: "editor" as ConversationTab,
|
||||
images: [],
|
||||
files: [],
|
||||
loadingFiles: [],
|
||||
loadingImages: [],
|
||||
messageToSend: null,
|
||||
shouldShownAgentLoading: false,
|
||||
submittedMessage: null,
|
||||
shouldHideSuggestions: false,
|
||||
hasRightPanelToggled: true,
|
||||
|
||||
// Actions
|
||||
setIsRightPanelShown: (isRightPanelShown) =>
|
||||
set({ isRightPanelShown }, false, "setIsRightPanelShown"),
|
||||
|
||||
setSelectedTab: (selectedTab) =>
|
||||
set({ selectedTab }, false, "setSelectedTab"),
|
||||
|
||||
setShouldShownAgentLoading: (shouldShownAgentLoading) =>
|
||||
set({ shouldShownAgentLoading }, false, "setShouldShownAgentLoading"),
|
||||
|
||||
setShouldHideSuggestions: (shouldHideSuggestions) =>
|
||||
set({ shouldHideSuggestions }, false, "setShouldHideSuggestions"),
|
||||
|
||||
addImages: (images) =>
|
||||
set(
|
||||
(state) => ({ images: [...state.images, ...images] }),
|
||||
false,
|
||||
"addImages",
|
||||
),
|
||||
|
||||
addFiles: (files) =>
|
||||
set(
|
||||
(state) => ({ files: [...state.files, ...files] }),
|
||||
false,
|
||||
"addFiles",
|
||||
),
|
||||
|
||||
removeImage: (index) =>
|
||||
set(
|
||||
(state) => {
|
||||
const newImages = [...state.images];
|
||||
newImages.splice(index, 1);
|
||||
return { images: newImages };
|
||||
},
|
||||
false,
|
||||
"removeImage",
|
||||
),
|
||||
|
||||
removeFile: (index) =>
|
||||
set(
|
||||
(state) => {
|
||||
const newFiles = [...state.files];
|
||||
newFiles.splice(index, 1);
|
||||
return { files: newFiles };
|
||||
},
|
||||
false,
|
||||
"removeFile",
|
||||
),
|
||||
|
||||
clearImages: () => set({ images: [] }, false, "clearImages"),
|
||||
|
||||
clearFiles: () => set({ files: [] }, false, "clearFiles"),
|
||||
|
||||
clearAllFiles: () =>
|
||||
set(
|
||||
{
|
||||
images: [],
|
||||
files: [],
|
||||
loadingFiles: [],
|
||||
loadingImages: [],
|
||||
},
|
||||
false,
|
||||
"clearAllFiles",
|
||||
),
|
||||
|
||||
addFileLoading: (fileName) =>
|
||||
set(
|
||||
(state) => {
|
||||
if (!state.loadingFiles.includes(fileName)) {
|
||||
return { loadingFiles: [...state.loadingFiles, fileName] };
|
||||
}
|
||||
return state;
|
||||
},
|
||||
false,
|
||||
"addFileLoading",
|
||||
),
|
||||
|
||||
removeFileLoading: (fileName) =>
|
||||
set(
|
||||
(state) => ({
|
||||
loadingFiles: state.loadingFiles.filter(
|
||||
(name) => name !== fileName,
|
||||
),
|
||||
}),
|
||||
false,
|
||||
"removeFileLoading",
|
||||
),
|
||||
|
||||
addImageLoading: (imageName) =>
|
||||
set(
|
||||
(state) => {
|
||||
if (!state.loadingImages.includes(imageName)) {
|
||||
return { loadingImages: [...state.loadingImages, imageName] };
|
||||
}
|
||||
return state;
|
||||
},
|
||||
false,
|
||||
"addImageLoading",
|
||||
),
|
||||
|
||||
removeImageLoading: (imageName) =>
|
||||
set(
|
||||
(state) => ({
|
||||
loadingImages: state.loadingImages.filter(
|
||||
(name) => name !== imageName,
|
||||
),
|
||||
}),
|
||||
false,
|
||||
"removeImageLoading",
|
||||
),
|
||||
|
||||
clearAllLoading: () =>
|
||||
set({ loadingFiles: [], loadingImages: [] }, false, "clearAllLoading"),
|
||||
|
||||
setMessageToSend: (text) =>
|
||||
set(
|
||||
{
|
||||
messageToSend: {
|
||||
text,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"setMessageToSend",
|
||||
),
|
||||
|
||||
setSubmittedMessage: (submittedMessage) =>
|
||||
set({ submittedMessage }, false, "setSubmittedMessage"),
|
||||
|
||||
resetConversationState: () =>
|
||||
set({ shouldHideSuggestions: false }, false, "resetConversationState"),
|
||||
|
||||
setHasRightPanelToggled: (hasRightPanelToggled) =>
|
||||
set({ hasRightPanelToggled }, false, "setHasRightPanelToggled"),
|
||||
}),
|
||||
{
|
||||
name: "conversation-store",
|
||||
},
|
||||
),
|
||||
);
|
||||
23
frontend/src/state/event-message-slice.tsx
Normal file
23
frontend/src/state/event-message-slice.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export const eventMessageSlice = createSlice({
|
||||
name: "eventMessage",
|
||||
initialState: {
|
||||
submittedEventIds: [] as number[], // Avoid the flashing issue of the confirmation buttons
|
||||
},
|
||||
reducers: {
|
||||
addSubmittedEventId: (state, action) => {
|
||||
state.submittedEventIds.push(action.payload);
|
||||
},
|
||||
removeSubmittedEventId: (state, action) => {
|
||||
state.submittedEventIds = state.submittedEventIds.filter(
|
||||
(id) => id !== action.payload,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { addSubmittedEventId, removeSubmittedEventId } =
|
||||
eventMessageSlice.actions;
|
||||
|
||||
export default eventMessageSlice.reducer;
|
||||
24
frontend/src/state/file-state-slice.ts
Normal file
24
frontend/src/state/file-state-slice.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
type SliceState = { changed: Record<string, boolean> }; // Map<path, changed>
|
||||
|
||||
const initialState: SliceState = {
|
||||
changed: {},
|
||||
};
|
||||
|
||||
export const fileStateSlice = createSlice({
|
||||
name: "fileState",
|
||||
initialState,
|
||||
reducers: {
|
||||
setChanged(
|
||||
state,
|
||||
action: PayloadAction<{ path: string; changed: boolean }>,
|
||||
) {
|
||||
const { path, changed } = action.payload;
|
||||
state.changed[path] = changed;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setChanged } = fileStateSlice.actions;
|
||||
export default fileStateSlice.reducer;
|
||||
56
frontend/src/state/microagent-management-slice.tsx
Normal file
56
frontend/src/state/microagent-management-slice.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { IMicroagentItem } from "#/types/microagent-management";
|
||||
|
||||
export const microagentManagementSlice = createSlice({
|
||||
name: "microagentManagement",
|
||||
initialState: {
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: null as GitRepository | null,
|
||||
personalRepositories: [] as GitRepository[],
|
||||
organizationRepositories: [] as GitRepository[],
|
||||
repositories: [] as GitRepository[],
|
||||
selectedMicroagentItem: null as IMicroagentItem | null,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
reducers: {
|
||||
setAddMicroagentModalVisible: (state, action) => {
|
||||
state.addMicroagentModalVisible = action.payload;
|
||||
},
|
||||
setUpdateMicroagentModalVisible: (state, action) => {
|
||||
state.updateMicroagentModalVisible = action.payload;
|
||||
},
|
||||
setSelectedRepository: (state, action) => {
|
||||
state.selectedRepository = action.payload;
|
||||
},
|
||||
setPersonalRepositories: (state, action) => {
|
||||
state.personalRepositories = action.payload;
|
||||
},
|
||||
setOrganizationRepositories: (state, action) => {
|
||||
state.organizationRepositories = action.payload;
|
||||
},
|
||||
setRepositories: (state, action) => {
|
||||
state.repositories = action.payload;
|
||||
},
|
||||
setSelectedMicroagentItem: (state, action) => {
|
||||
state.selectedMicroagentItem = action.payload;
|
||||
},
|
||||
setLearnThisRepoModalVisible: (state, action) => {
|
||||
state.learnThisRepoModalVisible = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setAddMicroagentModalVisible,
|
||||
setUpdateMicroagentModalVisible,
|
||||
setSelectedRepository,
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
setSelectedMicroagentItem,
|
||||
setLearnThisRepoModalVisible,
|
||||
} = microagentManagementSlice.actions;
|
||||
|
||||
export default microagentManagementSlice.reducer;
|
||||
@@ -1,76 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { IMicroagentItem } from "#/types/microagent-management";
|
||||
|
||||
interface MicroagentManagementState {
|
||||
// Modal visibility states
|
||||
addMicroagentModalVisible: boolean;
|
||||
updateMicroagentModalVisible: boolean;
|
||||
learnThisRepoModalVisible: boolean;
|
||||
|
||||
// Repository states
|
||||
selectedRepository: GitRepository | null;
|
||||
personalRepositories: GitRepository[];
|
||||
organizationRepositories: GitRepository[];
|
||||
repositories: GitRepository[];
|
||||
|
||||
// Microagent states
|
||||
selectedMicroagentItem: IMicroagentItem | null;
|
||||
}
|
||||
|
||||
interface MicroagentManagementActions {
|
||||
// Modal actions
|
||||
setAddMicroagentModalVisible: (visible: boolean) => void;
|
||||
setUpdateMicroagentModalVisible: (visible: boolean) => void;
|
||||
setLearnThisRepoModalVisible: (visible: boolean) => void;
|
||||
|
||||
// Repository actions
|
||||
setSelectedRepository: (repository: GitRepository | null) => void;
|
||||
setPersonalRepositories: (repositories: GitRepository[]) => void;
|
||||
setOrganizationRepositories: (repositories: GitRepository[]) => void;
|
||||
setRepositories: (repositories: GitRepository[]) => void;
|
||||
|
||||
// Microagent actions
|
||||
setSelectedMicroagentItem: (item: IMicroagentItem | null) => void;
|
||||
}
|
||||
|
||||
type MicroagentManagementStore = MicroagentManagementState &
|
||||
MicroagentManagementActions;
|
||||
|
||||
export const useMicroagentManagementStore = create<MicroagentManagementStore>(
|
||||
(set) => ({
|
||||
// Initial state
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem: null,
|
||||
|
||||
// Actions
|
||||
setAddMicroagentModalVisible: (visible: boolean) =>
|
||||
set({ addMicroagentModalVisible: visible }),
|
||||
|
||||
setUpdateMicroagentModalVisible: (visible: boolean) =>
|
||||
set({ updateMicroagentModalVisible: visible }),
|
||||
|
||||
setLearnThisRepoModalVisible: (visible: boolean) =>
|
||||
set({ learnThisRepoModalVisible: visible }),
|
||||
|
||||
setSelectedRepository: (repository: GitRepository | null) =>
|
||||
set({ selectedRepository: repository }),
|
||||
|
||||
setPersonalRepositories: (repositories: GitRepository[]) =>
|
||||
set({ personalRepositories: repositories }),
|
||||
|
||||
setOrganizationRepositories: (repositories: GitRepository[]) =>
|
||||
set({ organizationRepositories: repositories }),
|
||||
|
||||
setRepositories: (repositories: GitRepository[]) => set({ repositories }),
|
||||
|
||||
setSelectedMicroagentItem: (item: IMicroagentItem | null) =>
|
||||
set({ selectedMicroagentItem: item }),
|
||||
}),
|
||||
);
|
||||
60
frontend/src/state/security-analyzer-slice.ts
Normal file
60
frontend/src/state/security-analyzer-slice.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export enum ActionSecurityRisk {
|
||||
UNKNOWN = -1,
|
||||
LOW = 0,
|
||||
MEDIUM = 1,
|
||||
HIGH = 2,
|
||||
}
|
||||
|
||||
export type SecurityAnalyzerLog = {
|
||||
id: number;
|
||||
content: string;
|
||||
security_risk: ActionSecurityRisk;
|
||||
confirmation_state?: "awaiting_confirmation" | "confirmed" | "rejected";
|
||||
confirmed_changed: boolean;
|
||||
};
|
||||
|
||||
const initialLogs: SecurityAnalyzerLog[] = [];
|
||||
|
||||
export const securityAnalyzerSlice = createSlice({
|
||||
name: "securityAnalyzer",
|
||||
initialState: {
|
||||
logs: initialLogs,
|
||||
},
|
||||
reducers: {
|
||||
appendSecurityAnalyzerInput: (state, action) => {
|
||||
const log = {
|
||||
id: action.payload.id,
|
||||
content:
|
||||
action.payload.args.command ||
|
||||
action.payload.args.code ||
|
||||
action.payload.args.content ||
|
||||
action.payload.message,
|
||||
security_risk: action.payload.args.security_risk as ActionSecurityRisk,
|
||||
confirmation_state: action.payload.args.confirmation_state,
|
||||
confirmed_changed: false,
|
||||
};
|
||||
|
||||
const existingLog = state.logs.find(
|
||||
(stateLog) =>
|
||||
stateLog.id === log.id ||
|
||||
(stateLog.confirmation_state === "awaiting_confirmation" &&
|
||||
stateLog.content === log.content),
|
||||
);
|
||||
|
||||
if (existingLog) {
|
||||
if (existingLog.confirmation_state !== log.confirmation_state) {
|
||||
existingLog.confirmation_state = log.confirmation_state;
|
||||
existingLog.confirmed_changed = true;
|
||||
}
|
||||
} else {
|
||||
state.logs.push(log);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { appendSecurityAnalyzerInput } = securityAnalyzerSlice.actions;
|
||||
|
||||
export default securityAnalyzerSlice.reducer;
|
||||
@@ -1,10 +1,22 @@
|
||||
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||
import agentReducer from "./state/agent-slice";
|
||||
import browserReducer from "./state/browser-slice";
|
||||
import fileStateReducer from "./state/file-state-slice";
|
||||
import { jupyterReducer } from "./state/jupyter-slice";
|
||||
import securityAnalyzerReducer from "./state/security-analyzer-slice";
|
||||
import microagentManagementReducer from "./state/microagent-management-slice";
|
||||
import conversationReducer from "./state/conversation-slice";
|
||||
import eventMessageReducer from "./state/event-message-slice";
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
fileState: fileStateReducer,
|
||||
browser: browserReducer,
|
||||
agent: agentReducer,
|
||||
jupyter: jupyterReducer,
|
||||
securityAnalyzer: securityAnalyzerReducer,
|
||||
microagentManagement: microagentManagementReducer,
|
||||
conversation: conversationReducer,
|
||||
eventMessage: eventMessageReducer,
|
||||
});
|
||||
|
||||
const store = configureStore({
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface BrowserState {
|
||||
// URL of browser window (placeholder for now, will be replaced with the actual URL later)
|
||||
url: string;
|
||||
// Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later)
|
||||
screenshotSrc: string;
|
||||
}
|
||||
|
||||
interface BrowserStore extends BrowserState {
|
||||
setUrl: (url: string) => void;
|
||||
setScreenshotSrc: (screenshotSrc: string) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const initialState: BrowserState = {
|
||||
url: "https://github.com/All-Hands-AI/OpenHands",
|
||||
screenshotSrc: "",
|
||||
};
|
||||
|
||||
export const useBrowserStore = create<BrowserStore>((set) => ({
|
||||
...initialState,
|
||||
setUrl: (url: string) => set({ url }),
|
||||
setScreenshotSrc: (screenshotSrc: string) => set({ screenshotSrc }),
|
||||
reset: () => set(initialState),
|
||||
}));
|
||||
@@ -1,24 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface EventMessageState {
|
||||
submittedEventIds: number[]; // Avoid the flashing issue of the confirmation buttons
|
||||
}
|
||||
|
||||
interface EventMessageStore extends EventMessageState {
|
||||
addSubmittedEventId: (id: number) => void;
|
||||
removeSubmittedEventId: (id: number) => void;
|
||||
}
|
||||
|
||||
export const useEventMessageStore = create<EventMessageStore>((set) => ({
|
||||
submittedEventIds: [],
|
||||
addSubmittedEventId: (id: number) =>
|
||||
set((state) => ({
|
||||
submittedEventIds: [...state.submittedEventIds, id],
|
||||
})),
|
||||
removeSubmittedEventId: (id: number) =>
|
||||
set((state) => ({
|
||||
submittedEventIds: state.submittedEventIds.filter(
|
||||
(eventId) => eventId !== id,
|
||||
),
|
||||
})),
|
||||
}));
|
||||
@@ -1,75 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
export enum ActionSecurityRisk {
|
||||
UNKNOWN = -1,
|
||||
LOW = 0,
|
||||
MEDIUM = 1,
|
||||
HIGH = 2,
|
||||
}
|
||||
|
||||
export type SecurityAnalyzerLog = {
|
||||
id: number;
|
||||
content: string;
|
||||
security_risk: ActionSecurityRisk;
|
||||
confirmation_state?: "awaiting_confirmation" | "confirmed" | "rejected";
|
||||
confirmed_changed: boolean;
|
||||
};
|
||||
|
||||
interface SecurityAnalyzerState {
|
||||
logs: SecurityAnalyzerLog[];
|
||||
}
|
||||
|
||||
interface SecurityAnalyzerStore extends SecurityAnalyzerState {
|
||||
appendSecurityAnalyzerInput: (message: {
|
||||
id: number;
|
||||
args: {
|
||||
command?: string;
|
||||
code?: string;
|
||||
content?: string;
|
||||
security_risk: ActionSecurityRisk;
|
||||
confirmation_state?: "awaiting_confirmation" | "confirmed" | "rejected";
|
||||
};
|
||||
message?: string;
|
||||
}) => void;
|
||||
clearLogs: () => void;
|
||||
}
|
||||
|
||||
const initialLogs: SecurityAnalyzerLog[] = [];
|
||||
|
||||
export const useSecurityAnalyzerStore = create<SecurityAnalyzerStore>(
|
||||
(set) => ({
|
||||
logs: initialLogs,
|
||||
appendSecurityAnalyzerInput: (message) =>
|
||||
set((state) => {
|
||||
const log: SecurityAnalyzerLog = {
|
||||
id: message.id,
|
||||
content:
|
||||
message.args.command ||
|
||||
message.args.code ||
|
||||
message.args.content ||
|
||||
message.message ||
|
||||
"",
|
||||
security_risk: message.args.security_risk,
|
||||
confirmation_state: message.args.confirmation_state,
|
||||
confirmed_changed: false,
|
||||
};
|
||||
|
||||
const existingLog = state.logs.find(
|
||||
(stateLog) =>
|
||||
stateLog.id === log.id ||
|
||||
(stateLog.confirmation_state === "awaiting_confirmation" &&
|
||||
stateLog.content === log.content),
|
||||
);
|
||||
|
||||
if (existingLog) {
|
||||
if (existingLog.confirmation_state !== log.confirmation_state) {
|
||||
existingLog.confirmation_state = log.confirmation_state;
|
||||
existingLog.confirmed_changed = true;
|
||||
}
|
||||
return { logs: [...state.logs] }; // Return new array to trigger re-render
|
||||
}
|
||||
return { logs: [...state.logs, log] };
|
||||
}),
|
||||
clearLogs: () => set({ logs: initialLogs }),
|
||||
}),
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user