Compare commits

..

85 Commits

Author SHA1 Message Date
chuckbutkus
7c556d6396 Merge branch 'main' into chuck-build 2025-09-23 14:25:16 -04:00
openhands
8bb5aa21b9 test 2025-09-23 14:19:20 -04:00
BenYao21
d3d70fcc60 issue #9388, this will fix the issue (#10450)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-09-22 16:56:53 -04:00
Xinyi He
7906eab6b1 Add inference generation of SWE-Perf Benchmark (#10246)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-22 20:35:30 +00:00
juanmichelini
547e1049f1 Multi swe gym (#10605)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-22 15:56:26 -04:00
mamoodi
818cc60b52 New label for not going stale (#11069) 2025-09-22 11:53:47 -04:00
Robert Brennan
431d2c1f43 security: upgrade setuptools to >=78.1.1 to address CVE-2025-47273 and CVE-2024-6345 (#11038)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: enyst <engel.nyst@gmail.com>
2025-09-22 04:05:45 +00:00
Engel Nyst
07f23641a3 build(deps): pin litellm to avoid build failure (#11054)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-22 03:54:37 +02:00
Hiep Le
de84af5586 feat(frontend): display lock icon when confirmation mode is enabled (#11030) 2025-09-20 10:55:19 +07:00
Hiep Le
b7765ba3f7 refactor(frontend): fix typecheck (#11037) 2025-09-19 13:43:00 -04:00
Hiep Le
b89f2e51e4 refactor(frontend): migration of metrics-slice.ts to zustand (#11018) 2025-09-19 23:52:21 +07:00
mamoodi
e09f93aa75 Release 0.57.0 (#10981)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-09-19 12:40:56 -04:00
Hiep Le
9f529b105a refactor(frontend): migration of command-slice.ts to zustand (#11003) 2025-09-19 23:33:59 +07:00
Graham Neubig
89e3d2a867 Improve OpenHands provider pricing documentation (#10974)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-20 00:22:44 +08:00
Hiep Le
a7b9a4f291 refactor(frontend): migration of status-slice.ts to zustand (#11017) 2025-09-19 22:27:55 +07:00
Hiep Le
88cd16ae21 refactor(frontend): migration of initial-query-slice.ts to zustand (#11020) 2025-09-19 22:27:20 +07:00
Hiep Le
a8a3e9e604 refactor(frontend): remove the code-slice.ts file (#11021) 2025-09-19 21:22:29 +07:00
Hiep Le
0061bcc0b0 refactor(frontend): custom chat input (#10984) 2025-09-19 21:06:18 +07:00
Hiep Le
9c9fa780b0 refactor(frontend): task tracking observation content (#11002) 2025-09-19 20:03:05 +07:00
Alona
569ac16163 Improve token refresh error logging (#11026) 2025-09-19 14:18:38 +07:00
openhands
08096db29f test 2025-09-18 22:50:21 -04:00
openhands
b2b6ddf90c test 2025-09-18 22:24:35 -04:00
openhands
87fe36d811 test 2025-09-18 21:44:34 -04:00
openhands
39d255d313 test 2025-09-18 21:27:03 -04:00
openhands
e334b67f21 Add logging 2025-09-18 20:48:24 -04:00
Robert Brennan
46f7738f41 Update Python packages to latest versions (#11023)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 19:52:46 +00:00
Rohit Malhotra
3f3669dd34 Hotfix: rm model choice override (#11022) 2025-09-18 14:40:06 -04:00
sp.wack
cd65645eea Hide Tavily search API key help text in SaaS mode (#11014)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 16:40:29 +00:00
Robert Brennan
8e88a7a277 fix: resolve critical and high CVEs in enterprise Docker image (#10987)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 11:25:33 -04:00
Hiep Le
b393d52439 refactor(frontend): conversation main (#10985) 2025-09-18 20:23:13 +07:00
Hiep Le
faeec48365 refactor(frontend): conversation card (#10986) 2025-09-18 20:22:59 +07:00
chuckbutkus
d5c02bf87b Merge branch 'main' into allow-custom-user 2025-09-17 22:43:30 -04:00
openhands
14a4664fe8 Make su commands optional 2025-09-17 22:40:21 -04:00
sp.wack
774caf0607 feat: refactor status indicators (#10983)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 22:32:55 +04:00
chuckbutkus
3a7df33acf Merge branch 'main' into test-user 2025-09-17 14:02:52 -04:00
sp.wack
7222730df0 Fix SaaS callback URLs and pro pill positioning (#10998)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 16:56:02 +00:00
Hiep Le
910177fc57 refactor(frontend): system message modal (#10969) 2025-09-17 21:56:14 +07:00
Hiep Le
ac9badbd20 refactor(frontend): metrics modal (#10968) 2025-09-17 21:55:25 +07:00
Ray Myers
02c299d88f Fix Slack resolver failing on AWAITING_USER_INPUT state (#10992)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 09:20:12 -05:00
mamoodi
f65fbef649 Remove runtime settings (#10996) 2025-09-17 13:59:29 +00:00
Hiep Le
3c2acad28d refactor(frontend): microagents modal (#10970) 2025-09-16 22:32:23 +07:00
Boxuan Li
0f1780728e Update str_replace_editor tool to use dynamic workspace path from SANDBOX_VOLUMES (#10965)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-15 17:46:54 -07:00
sp.wack
d3f3378a4c feat: Upgrade banner for unsubscribed SaaS users (#10890)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-15 23:04:44 +00:00
Engel Nyst
65f4164749 [Docs] Add environment variables reference table (#10926)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-15 18:31:44 +00:00
Hiep Le
3f984d878b refactor(frontend): move conversation APIs to a dedicated service handler (#10957) 2025-09-16 00:57:15 +07:00
Eliot Jones
10b871f4ab feat: Add Cygnal integration (#10898) 2025-09-15 09:57:03 -04:00
Hiep Le
d664f516db refactor(frontend): conversation tab content component (#10956) 2025-09-15 20:56:38 +07:00
Hiep Le
e74bbd81d1 fix(frontend): suppressing event display in the absence of user messages (#10955) 2025-09-15 20:56:16 +07:00
Hiep Le
ab893f93f0 refactor(frontend): use-auto-resize hook (#10959) 2025-09-15 20:49:15 +07:00
Hiep Le
5aba498e77 refactor(frontend): move billing APIs to a dedicated service handler (#10958) 2025-09-15 20:37:07 +07:00
Hiep Le
1523555eea refactor(frontend): remove dead code (#10839) 2025-09-15 20:35:56 +07:00
Kaushik Ashodiya
30604c40fc fix: improve CLI help and version command performance (#10908)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-12 14:23:01 -04:00
Hiep Le
8dc46b7206 refactor(frontend): optimize pre-commit lint script (#10870)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-09-12 15:23:29 +00:00
Hiep Le
69498bebb4 refactor(frontend): new conversation component (#10937)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-09-12 22:15:26 +07:00
tksrmz
77ee9e25d9 fix(frontend): highlight preceding stars on hover in LikertScale (#10948) 2025-09-12 18:01:40 +04:00
Hiep Le
74753036bb refactor(frontend): move user APIs to a dedicated service handler (#10943) 2025-09-12 09:08:15 +07:00
Hiep Le
95d7c10608 refactor(frontend): move option APIs to a dedicated service handler (#10933) 2025-09-12 00:43:15 +07:00
Hiep Le
c142cc27ff refactor(frontend): home header component (#10930) 2025-09-12 00:10:58 +07:00
Hiep Le
0e20fc206b refactor(frontend): move settings APIs to a dedicated service handler (#10941) 2025-09-11 23:39:23 +07:00
Hiep Le
e21475a88e feat(frontend): persist drawer open/close state on page refresh (#10935)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-09-11 15:58:00 +00:00
Hiep Le
921fec0019 refactor(frontend): expand repository pill to full available width (#10936) 2025-09-11 22:37:44 +07:00
Hiep Le
049f839a62 refactor(frontend): move auth APIs to a dedicated service handler (#10932) 2025-09-11 22:31:41 +07:00
Hiep Le
0dde758e13 refactor(frontend): move microagent management API to a dedicated service handler (#10934) 2025-09-11 22:27:56 +07:00
Tim O'Farrell
8257ae70cc Additional logs to debug container working directories (#10902)
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
2025-09-11 11:06:19 -04:00
Ray Myers
4513bcc622 chore - MyPy check Enterprise with OpenHands (#10858)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-09-11 11:05:50 -04:00
Hiep Le
b5b9a3f40b refactor(frontend): create waiting for runtime component (#10931) 2025-09-11 21:30:05 +07:00
Xingyao Wang
8ea1259943 Add GitHub workflow for MDX format checking and fix parsing error (#10924)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-10 23:04:54 +00:00
Ray Myers
ddb2794adf fix - Tag enterprise with the same SHA as app image. (#10921) 2025-09-10 16:47:31 -05:00
sp.wack
79fdcad7ef Fix status indicator and chat input synchronization issue (#10914)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-10 20:39:14 +00:00
chuckbutkus
1de70b8ce4 Fix runtime init (#10909) 2025-09-10 19:28:12 +00:00
sp.wack
3baeecb27c meta(frontend): Improve UX (#9845)
Co-authored-by: Mislav Lukach <mislavlukach@gmail.com>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-10 18:12:52 +00:00
chuckbutkus
69fddecc7f Merge branch 'main' into test-user 2025-09-07 21:55:39 -04:00
Chuck Butkus
3afe5ccee5 Add Logging 2025-09-05 20:52:48 -04:00
chuckbutkus
3d5a8dcf5a Merge branch 'main' into test-user 2025-09-05 14:20:10 -04:00
Chuck Butkus
2ee1abe22c Lint fix 2025-09-05 13:16:03 -04:00
Chuck Butkus
148940f553 Added logging around alive checks 2025-09-05 11:10:57 -04:00
Chuck Butkus
1f09296136 Fix username checks 2025-09-03 21:40:13 -04:00
Chuck Butkus
70e5d12ba9 Revert "Change to a non-login shell"
This reverts commit bcb3160d95.
2025-08-29 01:48:47 -04:00
Chuck Butkus
bcb3160d95 Change to a non-login shell 2025-08-29 01:37:02 -04:00
Chuck Butkus
174c691744 Update 2025-08-28 02:25:05 -04:00
Chuck Butkus
af34d446e9 Remove vscode username restriction 2025-08-28 02:22:27 -04:00
Chuck Butkus
6604924f76 Fix bash username 2025-08-28 02:21:41 -04:00
chuckbutkus
b2def1e438 Merge branch 'main' into test-user 2025-08-27 23:33:45 -04:00
Chuck Butkus
2b8e47aca9 Add runtime user env vars 2025-08-27 23:02:39 -04:00
Chuck Butkus
dba8b28824 Logging 2025-08-27 21:30:47 -04:00
165 changed files with 2579 additions and 12480 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -489,47 +489,6 @@ type = "noop"
# Run the runtime sandbox container in privileged mode for use with docker-in-docker
#privileged = false
#################################### MCP #####################################
# Configuration for Model Context Protocol (MCP) servers
# MCP allows OpenHands to communicate with external tool servers
##############################################################################
[mcp]
# SSE servers - Server-Sent Events transport (legacy)
#sse_servers = [
# # Basic SSE server with just a URL
# "http://localhost:8080/mcp/sse",
#
# # SSE server with authentication
# {url = "https://api.example.com/mcp/sse", api_key = "your-api-key"}
#]
# SHTTP servers - Streamable HTTP transport (recommended)
#shttp_servers = [
# # Basic SHTTP server with default 60s timeout
# "https://api.example.com/mcp/shttp",
#
# # SHTTP server with custom timeout for long-running tools
# {
# url = "https://api.example.com/mcp/shttp",
# api_key = "your-api-key",
# timeout = 180 # 3 minutes for processing-heavy tools (1-3600 seconds)
# }
#]
# Stdio servers - Direct process communication (development only)
#stdio_servers = [
# # Basic stdio server
# {name = "filesystem", command = "npx", args = ["@modelcontextprotocol/server-filesystem", "/"]},
#
# # Stdio server with environment variables
# {
# name = "fetch",
# command = "uvx",
# args = ["mcp-server-fetch"],
# env = {DEBUG = "true"}
# }
#]
#################################### Model Routing ############################
# Configuration for experimental model routing feature
# Enables intelligent switching between different LLM models for specific purposes

View File

@@ -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

View File

@@ -67,19 +67,6 @@ sse_servers = [
# External MCP service with authentication
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
]
# SHTTP Servers - Modern streamable HTTP transport (recommended)
shttp_servers = [
# Basic SHTTP server with default 60s timeout
"https://api.example.com/mcp/shttp",
# Server with custom timeout for heavy operations
{
url = "https://files.example.com/mcp/shttp",
api_key = "your-api-key",
timeout = 1800 # 30 minutes for large file processing
}
]
```
@@ -131,17 +118,6 @@ SHTTP (Streamable HTTP) servers are configured using either a string URL or an o
- Type: `str`
- Description: API key for authentication
- `timeout` (optional)
- Type: `int`
- Default: `60`
- Range: `1-3600` seconds (1 hour maximum)
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
- **Use Cases:**
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations
- **Note**: This timeout only applies to individual tool calls, not server connection establishment.
### Stdio Servers
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
@@ -216,27 +192,5 @@ SHTTP is the modern HTTP-based transport protocol that provides enhanced feature
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
#### SHTTP Timeout Best Practices
When configuring SHTTP timeouts, consider these guidelines:
**Timeout Selection:**
- **Database queries**: 30-60 seconds
- **File operations**: 60-300 seconds (depending on file size)
- **Web scraping**: 60-120 seconds
- **Complex calculations**: 300-1800 seconds
- **Batch processing**: 1800-3600 seconds (maximum)
**Error Handling:**
When a tool call exceeds the configured timeout:
- The operation is cancelled with an `asyncio.TimeoutError`
- The agent receives a timeout error message
- The server connection remains active for subsequent requests
**Monitoring:**
- Set timeouts based on your tool's actual performance characteristics
- Monitor timeout occurrences to optimize timeout values
- Consider implementing server-side timeout handling for graceful degradation
### Standard Input/Output (stdio)
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.

1
force_build.txt Normal file
View File

@@ -0,0 +1 @@
test

View File

@@ -13,8 +13,7 @@ vi.mock("react-router", async () => {
vi.mock("#/context/conversation-context", () => ({
useConversation: () => ({ conversationId: "test-conversation-id" }),
ConversationProvider: ({ children }: { children: React.ReactNode }) =>
children,
ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
}));
vi.mock("react-i18next", async () => {
@@ -30,18 +29,21 @@ vi.mock("react-i18next", async () => {
};
});
// Mock Zustand browser store
// Mock redux
const mockDispatch = vi.fn();
let mockBrowserState = {
url: "https://example.com",
screenshotSrc: "",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
};
vi.mock("#/stores/browser-store", () => ({
useBrowserStore: () => mockBrowserState,
}));
vi.mock("react-redux", async () => {
const actual = await vi.importActual("react-redux");
return {
...actual,
useDispatch: () => mockDispatch,
useSelector: () => mockBrowserState,
};
});
// Import the component after all mocks are set up
import { BrowserPanel } from "#/components/features/browser/browser";
@@ -53,9 +55,6 @@ describe("Browser", () => {
mockBrowserState = {
url: "https://example.com",
screenshotSrc: "",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
};
});
@@ -64,9 +63,6 @@ describe("Browser", () => {
mockBrowserState = {
url: "https://example.com",
screenshotSrc: "",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
};
render(<BrowserPanel />);
@@ -79,11 +75,7 @@ describe("Browser", () => {
// Set the mock state for this test
mockBrowserState = {
url: "https://example.com",
screenshotSrc:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
};
render(<BrowserPanel />);

View File

@@ -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");

View File

@@ -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>,
);

View File

@@ -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>,
);

File diff suppressed because it is too large Load Diff

View File

@@ -7,38 +7,38 @@
"node": ">=22.0.0"
},
"dependencies": {
"@heroui/react": "^2.8.4",
"@heroui/react": "^2.8.3",
"@heroui/use-infinite-scroll": "^2.2.11",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.9.1",
"@react-router/serve": "^7.9.1",
"@react-router/node": "^7.8.2",
"@react-router/serve": "^7.8.2",
"@react-types/shared": "^3.32.0",
"@reduxjs/toolkit": "^2.9.0",
"@stripe/react-stripe-js": "^4.0.2",
"@stripe/react-stripe-js": "^4.0.0",
"@stripe/stripe-js": "^7.9.0",
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query": "^5.87.0",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.3",
"@vitejs/plugin-react": "^5.0.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.12.2",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"downshift": "^9.0.10",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.19",
"framer-motion": "^12.23.12",
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.30",
"jose": "^6.1.0",
"lucide-react": "^0.544.0",
"monaco-editor": "^0.53.0",
"posthog-js": "^1.268.1",
"lucide-react": "^0.542.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.261.7",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
@@ -47,7 +47,8 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.9.1",
"react-resizable-panels": "^3.0.5",
"react-router": "^7.8.2",
"react-syntax-highlighter": "^15.6.6",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
@@ -55,7 +56,7 @@
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"tailwind-scrollbar": "^4.0.2",
"vite": "^7.1.7",
"vite": "^7.1.4",
"web-vitals": "^5.1.0",
"ws": "^8.18.2",
"zustand": "^5.0.8"
@@ -97,16 +98,16 @@
"@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.55.1",
"@react-router/dev": "^7.9.1",
"@tailwindcss/typography": "^0.5.18",
"@tanstack/eslint-plugin-query": "^5.90.1",
"@playwright/test": "^1.55.0",
"@react-router/dev": "^7.8.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.86.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.5.2",
"@types/react": "^19.1.13",
"@types/node": "^24.3.1",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -128,8 +129,8 @@
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.2.0",
"husky": "^9.1.7",
"jsdom": "^27.0.0",
"lint-staged": "^16.2.0",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.6",
"msw": "^2.6.6",
"prettier": "^3.6.2",
"stripe": "^18.5.0",

View File

@@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.11.1'
const PACKAGE_VERSION = '2.10.5'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@@ -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,")

View File

@@ -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>

View File

@@ -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",

View File

@@ -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>

View File

@@ -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 (

View File

@@ -1,4 +1,4 @@
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
import {
FileWriteAction,
CommandAction,

View File

@@ -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>
);
}

View File

@@ -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}
/>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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";

View File

@@ -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}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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}
/>
)}
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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}
/>

View File

@@ -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>
);

View File

@@ -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"

View File

@@ -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>
);
}

View File

@@ -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 (

View File

@@ -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

View File

@@ -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}`}>

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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",
)}
>

View File

@@ -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 />;
}

View File

@@ -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} />;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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 (

View File

@@ -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) => {

View File

@@ -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 ?? {};

View File

@@ -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 ?? {};

View File

@@ -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();

View File

@@ -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 (

View File

@@ -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 ?? {};

View File

@@ -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 (

View File

@@ -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 ?? {};

View File

@@ -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

View File

@@ -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 ?? {};

View File

@@ -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">

View File

@@ -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>) => {

View File

@@ -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 ?? {};

View File

@@ -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 ?? {};

View File

@@ -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 (

View File

@@ -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 ?? {};

View File

@@ -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}
/>
)}
</>
)}

View File

@@ -8,7 +8,6 @@ interface MCPServerConfig {
name?: string;
url?: string;
api_key?: string;
timeout?: number;
command?: string;
args?: string[];
env?: Record<string, string>;

View File

@@ -8,7 +8,6 @@ interface MCPServerConfig {
name?: string;
url?: string;
api_key?: string;
timeout?: number;
command?: string;
args?: string[];
env?: Record<string, string>;

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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],
);

View File

@@ -1,10 +1,15 @@
import { useRef, useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
setMessageToSend,
setIsRightPanelShown,
} from "#/state/conversation-slice";
import { RootState } from "#/store";
import {
isContentEmpty,
clearEmptyContent,
getTextContent,
} from "#/components/features/chat/utils/chat-input.utils";
import { useConversationStore } from "#/state/conversation-store";
/**
* Hook for managing chat input content logic
@@ -12,21 +17,20 @@ import { useConversationStore } from "#/state/conversation-store";
export const useChatInputLogic = () => {
const chatInputRef = useRef<HTMLDivElement | null>(null);
const {
messageToSend,
hasRightPanelToggled,
setMessageToSend,
setIsRightPanelShown,
} = useConversationStore();
const { messageToSend, hasRightPanelToggled } = useSelector(
(state: RootState) => state.conversation,
);
const dispatch = useDispatch();
// Save current input value when drawer state changes
useEffect(() => {
if (chatInputRef.current) {
const currentText = getTextContent(chatInputRef.current);
setMessageToSend(currentText);
setIsRightPanelShown(hasRightPanelToggled);
dispatch(setMessageToSend(currentText));
dispatch(setIsRightPanelShown(hasRightPanelToggled));
}
}, [hasRightPanelToggled, setMessageToSend, setIsRightPanelShown]);
}, [hasRightPanelToggled, dispatch]);
// Helper function to check if contentEditable is truly empty
const checkIsContentEmpty = useCallback(

View File

@@ -1,4 +1,4 @@
import React, { useRef, useCallback, useState, useEffect } from "react";
import React, { useRef, useCallback, useState } from "react";
interface UseFileHandlingReturn {
fileInputRef: React.RefObject<HTMLInputElement | null>;
@@ -31,25 +31,6 @@ export const useFileHandling = (
[onFilesPaste],
);
// Listen for paste events with files
useEffect(() => {
const handlePasteFiles = (event: CustomEvent) => {
const files = event.detail.files as File[];
if (files && files.length > 0) {
addFiles(files);
}
};
document.addEventListener("pasteFiles", handlePasteFiles as EventListener);
return () => {
document.removeEventListener(
"pasteFiles",
handlePasteFiles as EventListener,
);
};
}, [addFiles]);
// File icon click handler
const handleFileIconClick = useCallback((isDisabled: boolean) => {
if (!isDisabled && fileInputRef.current) {

View File

@@ -1,10 +1,11 @@
import { useRef, useState, useCallback } from "react";
import { useDispatch } from "react-redux";
import { useAutoResize } from "#/hooks/use-auto-resize";
import { CHAT_INPUT } from "#/utils/constants";
import {
IMessageToSend,
useConversationStore,
} from "#/state/conversation-store";
setShouldHideSuggestions,
} from "#/state/conversation-slice";
import { CHAT_INPUT } from "#/utils/constants";
/**
* Hook for managing grip resize functionality
@@ -13,11 +14,11 @@ export const useGripResize = (
chatInputRef: React.RefObject<HTMLDivElement | null>,
messageToSend: IMessageToSend | null,
) => {
const gripRef = useRef<HTMLDivElement | null>(null);
const [isGripVisible, setIsGripVisible] = useState(false);
const { setShouldHideSuggestions } = useConversationStore();
const gripRef = useRef<HTMLDivElement | null>(null);
const dispatch = useDispatch();
// Drag state management callbacks
const handleDragStart = useCallback(() => {
@@ -47,9 +48,9 @@ export const useGripResize = (
(height: number) => {
// Hide suggestions when input height exceeds the threshold
const shouldHideChatSuggestions = height > CHAT_INPUT.HEIGHT_THRESHOLD;
setShouldHideSuggestions(shouldHideChatSuggestions);
dispatch(setShouldHideSuggestions(shouldHideChatSuggestions));
},
[setShouldHideSuggestions],
[dispatch],
);
// Use the auto-resize hook with height change callback

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, RefObject } from "react";
import { IMessageToSend } from "#/state/conversation-store";
import { IMessageToSend } from "#/state/conversation-slice";
import { useDragResize } from "./use-drag-resize";
// Constants

View File

@@ -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,
};
}

View File

@@ -806,9 +806,6 @@ export enum I18nKey {
SETTINGS$MCP_ERROR_COMMAND_NO_SPACES = "SETTINGS$MCP_ERROR_COMMAND_NO_SPACES",
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER = "SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER",
SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE = "SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE",
SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED = "SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED",
SETTINGS$MCP_SERVER_TYPE = "SETTINGS$MCP_SERVER_TYPE",
SETTINGS$MCP_API_KEY_PLACEHOLDER = "SETTINGS$MCP_API_KEY_PLACEHOLDER",
SETTINGS$MCP_COMMAND_ARGUMENTS = "SETTINGS$MCP_COMMAND_ARGUMENTS",

View File

@@ -12895,54 +12895,6 @@
"de": "Environment variables must follow KEY=value format",
"uk": "Environment variables must follow KEY=value format"
},
"SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER": {
"en": "Timeout must be a valid number",
"ja": "タイムアウトは有効な数値である必要があります",
"zh-CN": "超时必须是有效数字",
"zh-TW": "超時必須是有效數字",
"ko-KR": "타임아웃은 유효한 숫자여야 합니다",
"no": "Timeout må være et gyldig tall",
"it": "Il timeout deve essere un numero valido",
"pt": "O timeout deve ser um número válido",
"es": "El timeout debe ser un número válido",
"ar": "يجب أن يكون المهلة الزمنية رقمًا صالحًا",
"fr": "Le timeout doit être un nombre valide",
"tr": "Zaman aşımı geçerli bir sayı olmalıdır",
"de": "Timeout muss eine gültige Zahl sein",
"uk": "Таймаут повинен бути дійсним числом"
},
"SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE": {
"en": "Timeout must be positive",
"ja": "タイムアウトは正の値である必要があります",
"zh-CN": "超时必须为正数",
"zh-TW": "超時必須為正數",
"ko-KR": "타임아웃은 양수여야 합니다",
"no": "Timeout må være positiv",
"it": "Il timeout deve essere positivo",
"pt": "O timeout deve ser positivo",
"es": "El timeout debe ser positivo",
"ar": "يجب أن تكون المهلة الزمنية موجبة",
"fr": "Le timeout doit être positif",
"tr": "Zaman aşımı pozitif olmalıdır",
"de": "Timeout muss positiv sein",
"uk": "Таймаут повинен бути позитивним"
},
"SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED": {
"en": "Timeout cannot exceed 3600 seconds (1 hour)",
"ja": "タイムアウトは3600秒1時間を超えることはできません",
"zh-CN": "超时不能超过3600秒1小时",
"zh-TW": "超時不能超過3600秒1小時",
"ko-KR": "타임아웃은 3600초(1시간)을 초과할 수 없습니다",
"no": "Timeout kan ikke overstige 3600 sekunder (1 time)",
"it": "Il timeout non può superare 3600 secondi (1 ora)",
"pt": "O timeout não pode exceder 3600 segundos (1 hora)",
"es": "El timeout no puede exceder 3600 segundos (1 hora)",
"ar": "لا يمكن أن تتجاوز المهلة الزمنية 3600 ثانية (ساعة واحدة)",
"fr": "Le timeout ne peut pas dépasser 3600 secondes (1 heure)",
"tr": "Zaman aşımı 3600 saniyeyi (1 saat) aşamaz",
"de": "Timeout kann 3600 Sekunden (1 Stunde) nicht überschreiten",
"uk": "Таймаут не може перевищувати 3600 секунд (1 година)"
},
"SETTINGS$MCP_SERVER_TYPE": {
"en": "Server Type",
"ja": "サーバータイプ",

View File

@@ -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>

View File

@@ -20,7 +20,6 @@ interface MCPServerConfig {
name?: string;
url?: string;
api_key?: string;
timeout?: number;
command?: string;
args?: string[];
env?: Record<string, string>;
@@ -68,7 +67,6 @@ function MCPSettingsScreen() {
type: "shttp" as const,
url: typeof server === "string" ? server : server.url,
api_key: typeof server === "object" ? server.api_key : undefined,
timeout: typeof server === "object" ? server.timeout : undefined,
})),
];

View File

@@ -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));
}
}

View File

@@ -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:

View 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;

View 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;

View File

@@ -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",
},
),
);

View 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;

View 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;

View 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;

View File

@@ -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 }),
}),
);

View 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;

View File

@@ -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({

View File

@@ -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),
}));

View File

@@ -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,
),
})),
}));

View File

@@ -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