Add isolated dev stack command for OpenHands Cloud debugging (#20)

* Warn about OpenHands Cloud backend conflicts

Co-authored-by: openhands <openhands@all-hands.dev>

* Add isolated dev stack command

Co-authored-by: openhands <openhands@all-hands.dev>

* Promote isolated stack to default dev command

Co-authored-by: openhands <openhands@all-hands.dev>

* Fix dev-safe startup failure and docs

Co-authored-by: openhands <openhands@all-hands.dev>

* Restructure docs for users and developers

Co-authored-by: openhands <openhands@all-hands.dev>

* Remove libtmux from install docs

Co-authored-by: openhands <openhands@all-hands.dev>

* Fix first-load Vite optimize dependency errors

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig
2026-04-26 11:09:39 -04:00
committed by GitHub
co-authored by openhands
parent 72eef808f0
commit 0e60826123
10 changed files with 435 additions and 258 deletions
+5 -1
View File
@@ -1,8 +1,12 @@
# OpenHands Agent Server target
# These defaults assume you are manually pointing the frontend at a backend on
# 127.0.0.1:8000. The recommended local workflow is `npm run dev`, which starts
# an isolated local backend for this checkout. Use `npm run dev:frontend` only
# when you intentionally want to point at a separately managed backend.
VITE_BACKEND_HOST="127.0.0.1:8000" # Host:port used by the Vite dev proxy
VITE_BACKEND_BASE_URL="http://127.0.0.1:8000" # Base URL used by browser-side direct requests
# VITE_SESSION_API_KEY="" # Set to the same value as backend SESSION_API_KEY or OH_SESSION_API_KEYS_0 when auth is enabled
# VITE_WORKING_DIR="/workspace/project" # Workspace path used when starting conversations
# VITE_WORKING_DIR="/workspace/project/agent-server-gui" # Repo root used when starting conversations
# VITE_WORKER_URLS="" # Optional comma-separated worker URLs for the Browser tab
# VITE_ENABLE_BROWSER_TOOLS="true" # Set to false to omit BrowserToolSet from new conversations
+9 -3
View File
@@ -40,19 +40,25 @@
- `GET /api/conversations` expects repeated `ids` params (`?ids=a&ids=b`), not Axios's default bracket form (`ids[]=a`), so the shared Axios client needs a custom params serializer.
- Runtime git panels should prefer the conversation's reported `workspace.working_dir` when present; falling back to `/workspace/project` can produce 500s like `Not a git repository` for direct local workspaces such as `/workspace/project/agent-server-gui`.
- For the current `openhands-agent-server` PyPI/uv-tool flow, `uv tool install -U openhands-agent-server` alone was not sufficient in this environment. A working install was:
- `uv tool install -U --with openhands-tools --with openhands-workspace --with libtmux openhands-agent-server`
- `uv tool install -U --with openhands-tools --with openhands-workspace openhands-agent-server`
- `uv tool install` exposes the executable as `agent-server`, not `openhands-agent-server`, and may require adding `~/.local/bin` to `PATH`.
- Current SDK / agent-server conversation start payloads must use SDK-registered snake_case tool names, not the old class-style names. Working names against SDK v1.18.1 were:
- `terminal`
- `file_editor`
- `task_tracker`
- `browser_tool_set`
Using `TerminalTool` / `FileEditorTool` / `TaskTrackerTool` / `BrowserToolSet` caused live `/api/conversations/{id}/events` runs to fail with `ToolDefinition '<name>' is not registered`.
Using `TerminalTool` / `FileEditorTool` / `TaskTrackerTool` / `BrowserToolSet` caused live `/api/conversations/{id}/events` runs to fail with `ToolDefinition '<name>' is not registered`.
- The root compatibility bootstrap now treats `/server_info` network/timeout failures as a first-class `AgentServerUnavailableError`, uses a short 5s timeout for that probe, and disables React Query retries/toasts for the initial config fetch so missing backends fail fast with an explicit full-screen notice.
- For local verification in this repo, setting `VITE_WORKING_DIR=/workspace/project/agent-server-gui` avoids initial Changes-tab 500s from pointing conversations at the non-repo parent `/workspace/project`.
- OpenHands Cloud sandbox development note: do **not** reuse the sandbox's existing agent-server for this frontend. Current agent-server releases use a shared `openhands` tmux socket and default `workspace/conversations`, so a naive second server in the same sandbox can kill the cloud session's tmux state or attach to the same persisted conversations. `npm run dev` is now the recommended local workflow and starts an isolated local agent-server for the checkout by overriding `TMUX_TMPDIR`, `OH_CONVERSATIONS_PATH`, `OH_BASH_EVENTS_DIR`, and `OH_VSCODE_PORT` under `.openhands-dev/`; use `npm run dev:frontend` only when intentionally pointing at a separately managed backend, or `npm run dev:mock` for mock mode.
- A successful end-to-end live run in this environment required a real LLM config (`LLM_MODEL` + `LLM_API_KEY`). The default `litellm_proxy/...` model with no `llm_api_key` failed at runtime with a `litellm.AuthenticationError`.
- README expectation: the very first section should be a concrete from-scratch quickstart for running this frontend against a real `openhands-agent-server` (clone, install backend, optional `.env`, run `npm run dev`). Keep live-backend instructions ahead of general project overview.
- README expectation: keep the first section as a concrete, chronological from-scratch quickstart for running this frontend against a real `openhands-agent-server` (clone, install backend, optional `.env`, run `npm run dev`).
- Keep README user-focused and move contributor/developer-specific workflows (Cloud sandbox debugging, `dev:safe`, mock mode, detailed env vars/build-test notes) into `DEVELOPMENT.md`.
- `scripts/dev-safe.mjs` should fail fast if `agent-server` cannot be spawned (for example missing PATH entries).
- Vite dev mode can black-screen on first load with `504 Outdated Optimize Dep` if core client-entry deps are not prebundled; keep `react`, `react/jsx-runtime`, `react-dom/client`, and `react-router/dom` in `optimizeDeps.include`.
- As an OpenHands incubator **Sandbox** project, the repo should carry the standard sandbox warning badge in `README.md` and include a root `LICENSE` file to satisfy the incubator-program requirements.
- OpenHands repo bootstrap files live under `.openhands/`:
- `.openhands/setup.sh` installs frontend dependencies with `npm ci` when needed, creates `.env` from `.env.sample` if missing, appends `VITE_WORKING_DIR` for this repo when unset, and generates `src/i18n/declaration.ts` via `npm run make-i18n`.
+87
View File
@@ -0,0 +1,87 @@
# Development
This document is for contributors working on `agent-server-gui` itself.
## Recommended local workflow
The default development command is:
```sh
npm run dev
```
This is an alias for `npm run dev:safe`.
It starts a dedicated local `agent-server` for this checkout on `127.0.0.1:18000` and points the frontend at it. It isolates tmux state and conversation persistence by setting separate `TMUX_TMPDIR`, `OH_CONVERSATIONS_PATH`, `OH_BASH_EVENTS_DIR`, and `OH_VSCODE_PORT` values under `.openhands-dev/`, so it does not collide with other local or cloud-backed OpenHands sessions.
Useful overrides:
- `OH_GUI_SAFE_BACKEND_PORT` — backend port for the isolated server (default `18000`)
- `OH_GUI_SAFE_VSCODE_PORT` — VS Code sidecar port (default `backend port + 1`)
- `OH_GUI_SAFE_STATE_DIR` — base directory for isolated server state
- `VITE_WORKING_DIR` — repo root used for new conversations (defaults to the current checkout)
## OpenHands Cloud sandbox warning
If you are editing this repo from an OpenHands Cloud sandbox: **do not point this frontend at the sandbox's existing agent-server**.
Current `agent-server` releases share the default `openhands` tmux socket and `workspace/conversations` persistence directory, so a naive second server in the same sandbox can break the OpenHands conversation that is powering your cloud session (for example with errors like `no server running on /tmp/tmux-*/openhands`).
Use `npm run dev` / `npm run dev:safe` so this repo launches its own isolated backend.
## Alternative development workflows
### Frontend against an existing backend
Use this only if you intentionally started `agent-server` yourself or want the frontend to talk to another backend:
```sh
npm run dev:frontend
```
The frontend-only workflow expects the backend at `127.0.0.1:8000` by default.
If you start the backend with `SESSION_API_KEY` or `OH_SESSION_API_KEYS_0`, every `/api/*` route is authenticated with `X-Session-API-Key`. In that case the frontend must send the same key via `VITE_SESSION_API_KEY`.
### Mock mode
If you want to run the frontend without a live backend, use:
```sh
npm run dev:mock
# or
npm run dev:mock:saas
```
## Build and test
```sh
npm run test
npm run build
npm run start
```
Useful targeted verification for the isolated dev launcher:
```sh
npm run test -- __tests__/api/agent-server-config.test.ts __tests__/scripts/dev-safe.test.ts
```
## Environment variables
You can create a `.env` file in the project directory with these variables based on `.env.sample`.
| Variable | Description | Default Value |
| --------------------------- | ---------------------------------------------------------------------------------- | ---------------------- |
| `VITE_BACKEND_BASE_URL` | Full base URL for the agent server used by direct browser requests | current browser origin |
| `VITE_BACKEND_HOST` | Backend host used by the Vite dev proxy | `127.0.0.1:8000` |
| `VITE_SESSION_API_KEY` | Optional `X-Session-API-Key` header value for authenticated agent_server instances | - |
| `VITE_WORKING_DIR` | Workspace path sent when starting new conversations | `/workspace/project` |
| `VITE_WORKER_URLS` | Optional comma-separated worker/app URLs for the Browser tab | - |
| `VITE_ENABLE_BROWSER_TOOLS` | Set to `false` to omit `BrowserToolSet` from new conversation payloads | `true` |
| `VITE_MOCK_API` | Enable/disable API mocking with MSW | `false` |
| `VITE_MOCK_SAAS` | Simulate SaaS mode in development | `false` |
| `VITE_USE_TLS` | Use HTTPS/WSS for the Vite proxy target | `false` |
| `VITE_FRONTEND_PORT` | Port to run the frontend application | `3001` |
| `VITE_INSECURE_SKIP_VERIFY` | Skip TLS certificate verification for proxied backend requests | `false` |
| `VITE_GITHUB_TOKEN` | GitHub token for repository access (used in some tests) | - |
+15 -253
View File
@@ -5,15 +5,15 @@
[![Project Status: Sandbox](https://img.shields.io/badge/status-sandbox-yellow)](https://github.com/OpenHands/incubator-program)
## Run this with OpenHands Agent Server first
## Quickstart
If you only read one section of this README, read this one. Most users will want to clone this repo, point it at a real `openhands-agent-server`, and start using the UI immediately.
This repository is a near-direct port of the OpenHands frontend adapted to talk directly to `software-agent-sdk` / `agent_server` without the usual OpenHands app backend.
### Prerequisites
- Node.js 22.12.x or later
- `npm`
- A running OpenHands Agent Server (`agent-server`)
- OpenHands Agent Server (`agent-server`) installed and available on your `PATH`
### 1. Clone and install the frontend
@@ -23,7 +23,7 @@ cd agent-server-gui
npm install
```
### 2. Install and start OpenHands Agent Server
### 2. Install OpenHands Agent Server
If you do not already have the backend installed, install `uv` first (OpenHands SDK recommends `uv` 0.8.13+):
@@ -31,64 +31,41 @@ If you do not already have the backend installed, install `uv` first (OpenHands
curl -LsSf https://astral.sh/uv/install.sh | sh
```
Then install or upgrade the agent server package together with the tool/workspace dependencies that the SDK getting started page lists separately:
Then install or upgrade the agent server package together with the tool/workspace dependencies it needs:
```sh
uv tool install -U \
--with openhands-tools \
--with openhands-workspace \
--with libtmux \
openhands-agent-server
```
`uv tool install` exposes the server as the `agent-server` CLI. If `~/.local/bin` is not already on your `PATH`, either add it or run the binary via its full path.
Then start the backend on the default local port:
`uv tool install` exposes the server as the `agent-server` CLI. If `~/.local/bin` is not already on your `PATH`, add it before continuing:
```sh
agent-server --host 127.0.0.1 --port 8000
export PATH="$HOME/.local/bin:$PATH"
command -v agent-server
```
The frontend expects the backend at `127.0.0.1:8000` by default, so this is the easiest from-scratch setup.
If you start the backend with `SESSION_API_KEY` or `OH_SESSION_API_KEYS_0`, every `/api/*` route is authenticated with `X-Session-API-Key`. In that case the frontend must send the same key via `VITE_SESSION_API_KEY`.
If you prefer installing from source or want the full SDK setup flow, see the OpenHands SDK docs: <https://docs.openhands.dev/sdk/getting-started>
### 3. Optional: create a `.env` file
If your backend is **not** running on `127.0.0.1:8000`, or if it requires a session API key, create a `.env` file:
If you need to change the backend URL, frontend port, session API key, or working directory, copy the sample file:
```sh
cp .env.sample .env
```
Then update the values you need:
Then edit the values you need.
```dotenv
VITE_BACKEND_HOST="127.0.0.1:8000"
VITE_BACKEND_BASE_URL="http://127.0.0.1:8000"
VITE_FRONTEND_PORT="3001"
# Use the same value as backend SESSION_API_KEY or OH_SESSION_API_KEYS_0 when auth is enabled.
# VITE_SESSION_API_KEY="your-session-api-key"
# VITE_WORKING_DIR="/absolute/path/to/the-workspace-used-by-agent-server"
```
Notes:
- `VITE_BACKEND_HOST` is used by the Vite dev proxy for `/api`, `/server_info`, and `/sockets`.
- `VITE_BACKEND_BASE_URL` is used by browser-side direct requests. Keep it pointed at the same backend.
- `VITE_WORKING_DIR` should match the workspace path the backend will use when starting new conversations.
- If the backend is secured with `SESSION_API_KEY` or `OH_SESSION_API_KEYS_0`, set `VITE_SESSION_API_KEY` to the same value or live requests will fail with `401 Unauthorized`.
- If your backend does not require `X-Session-API-Key`, leave `VITE_SESSION_API_KEY` unset.
### 4. Start the frontend
### 4. Start the app
```sh
npm run dev
```
This starts the frontend on [http://localhost:3001](http://localhost:3001).
This starts an isolated local `agent-server` for this checkout and the frontend on [http://localhost:3001](http://localhost:3001).
### 5. First-run sanity check
@@ -96,224 +73,9 @@ After the page opens:
- `/` should load without errors
- `/settings` should load
- on secured backends, make sure `VITE_SESSION_API_KEY` matches the backend session key
- configure a working LLM model + API key under `Settings > LLM` before running the first live task
- if your backend is too old to expose settings schemas, the UI will show an actionable `SDK settings schema unavailable.` message instead of crashing
- configure a working LLM model + API key under `Settings > LLM` before running the first live task
- you should be able to open or create a conversation
- `/conversations/:id` should load conversation content
- if you want the Git / Changes panels to point at this repo, set `VITE_WORKING_DIR` to the actual repo root (for this checkout that is `/workspace/project/agent-server-gui`), not just the parent workspace directory
### Mock mode
## More documentation
If you want to run the frontend without a live backend, use:
```sh
npm run dev:mock
# or
npm run dev:mock:saas
```
## Overview
This repository is a near-direct port of the OpenHands frontend adapted to talk directly to `software-agent-sdk` / `agent_server` without the usual OpenHands app backend.
## Tech Stack
- Remix SPA Mode (React + Vite + React Router)
- TypeScript
- Redux
- TanStack Query
- Tailwind CSS
- i18next
- React Testing Library
- Vitest
- Mock Service Worker
## Building for Production
There is no `Makefile` in this repository. Use the npm scripts instead:
```sh
npm run build
npm run start
```
## Environment Variables
The frontend application uses the following environment variables:
| Variable | Description | Default Value |
| --------------------------- | ------------------------------------------------------------------------------------ | ------------------------ |
| `VITE_BACKEND_BASE_URL` | Full base URL for the agent server used by direct browser requests | current browser origin |
| `VITE_BACKEND_HOST` | Backend host used by the Vite dev proxy | `127.0.0.1:8000` |
| `VITE_SESSION_API_KEY` | Optional `X-Session-API-Key` header value for authenticated agent_server instances | - |
| `VITE_WORKING_DIR` | Workspace path sent when starting new conversations | `/workspace/project` |
| `VITE_WORKER_URLS` | Optional comma-separated worker/app URLs for the Browser tab | - |
| `VITE_ENABLE_BROWSER_TOOLS` | Set to `false` to omit `BrowserToolSet` from new conversation payloads | `true` |
| `VITE_MOCK_API` | Enable/disable API mocking with MSW | `false` |
| `VITE_MOCK_SAAS` | Simulate SaaS mode in development | `false` |
| `VITE_USE_TLS` | Use HTTPS/WSS for the Vite proxy target | `false` |
| `VITE_FRONTEND_PORT` | Port to run the frontend application | `3001` |
| `VITE_INSECURE_SKIP_VERIFY` | Skip TLS certificate verification for proxied backend requests | `false` |
| `VITE_GITHUB_TOKEN` | GitHub token for repository access (used in some tests) | - |
You can create a `.env` file in the project directory with these variables based on `.env.sample`.
### Project Structure
```sh
frontend
├── __tests__ # Tests
├── public
├── src
│ ├── api # API calls
│ ├── assets
│ ├── components
│ ├── context # Local state management
│ ├── hooks # Custom hooks
│ ├── i18n # Internationalization
│ ├── mocks # MSW mocks for development
│ ├── routes # React Router file-based routes
│ ├── services
│ ├── state # Redux state management
│ ├── types
│ ├── utils # Utility/helper functions
│ └── root.tsx # Entry point
└── .env.sample # Sample environment variables
```
#### Components
Components are organized into folders based on their **domain**, **feature**, or **shared functionality**.
```sh
components
├── features # Domain-specific components
├── layout
├── modals
└── ui # Shared UI components
```
### Features
- Real-time updates with WebSockets
- Internationalization
- Router data loading with Remix
- User authentication with GitHub OAuth (if saas mode is enabled)
## Testing
### Testing Framework and Tools
We use the following testing tools:
- **Test Runner**: Vitest
- **Rendering**: React Testing Library
- **User Interactions**: @testing-library/user-event
- **API Mocking**: [Mock Service Worker (MSW)](https://mswjs.io/)
- **Code Coverage**: Vitest with V8 coverage
### Running Tests
To run all tests:
```sh
npm run test
```
To run tests with coverage:
```sh
npm run test:coverage
```
### Testing Best Practices
1. **Component Testing**
- Test components in isolation
- Use our custom [`renderWithProviders()`](https://github.com/OpenHands/OpenHands/blob/ce26f1c6d3feec3eedf36f823dee732b5a61e517/frontend/test-utils.tsx#L56-L85) that wraps the components we want to test in our providers. It is especially useful for components that use Redux
- Use `render()` from React Testing Library to render components
- Prefer querying elements by role, label, or test ID over CSS selectors
- Test both rendering and interaction scenarios
2. **User Event Simulation**
- Use `userEvent` for simulating realistic user interactions
- Test keyboard events, clicks, typing, and other user actions
- Handle edge cases like disabled states, empty inputs, etc.
3. **Mocking**
- We test components that make network requests by mocking those requests with Mock Service Worker (MSW)
- Use `vi.fn()` to create mock functions for callbacks and event handlers
- Mock external dependencies and API calls (more info)[https://mswjs.io/docs/getting-started]
- Verify mock function calls using `.toHaveBeenCalledWith()`, `.toHaveBeenCalledTimes()`
4. **Accessibility Testing**
- Use `toBeInTheDocument()` to check element presence
- Test keyboard navigation and screen reader compatibility
- Verify correct ARIA attributes and roles
5. **State and Prop Testing**
- Test component behavior with different prop combinations
- Verify state changes and conditional rendering
- Test error states and loading scenarios
6. **Internationalization (i18n) Testing**
- Test translation keys and placeholders
- Verify text rendering across different languages
Example Test Structure:
```typescript
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
describe("ComponentName", () => {
it("should render correctly", () => {
render(<Component />);
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("should handle user interactions", async () => {
const mockCallback = vi.fn();
const user = userEvent.setup();
render(<Component onClick={mockCallback} />);
const button = screen.getByRole("button");
await user.click(button);
expect(mockCallback).toHaveBeenCalledOnce();
});
});
```
### Example Tests in the Codebase
For real-world examples of testing, check out these test files:
1. **Chat Input Component Test**:
[`__tests__/components/chat/chat-input.test.tsx`](https://github.com/OpenHands/OpenHands/blob/main/frontend/__tests__/components/chat/chat-input.test.tsx)
- Demonstrates comprehensive testing of a complex input component
- Covers various scenarios like submission, disabled states, and user interactions
2. **File Explorer Component Test**:
[`__tests__/components/file-explorer/file-explorer.test.tsx`](https://github.com/OpenHands/OpenHands/blob/main/frontend/__tests__/components/file-explorer/file-explorer.test.tsx)
- Shows testing of a more complex component with multiple interactions
- Illustrates testing of nested components and state management
### Test Coverage
- Aim for high test coverage, especially for critical components
- Focus on testing different scenarios and edge cases
- Use code coverage reports to identify untested code paths
### Continuous Integration
Tests are automatically run during:
- Pre-commit hooks
- Pull request checks
- CI/CD pipeline
## Contributing
Please read the [CONTRIBUTING.md](../CONTRIBUTING.md) file for details on our code of conduct, and the process for submitting pull requests to us.
## Troubleshooting
TODO
For contributor and developer workflows, including OpenHands Cloud sandbox debugging, frontend-only mode, mock mode, environment variables, and build/test commands, see [DEVELOPMENT.md](./DEVELOPMENT.md).
@@ -40,3 +40,4 @@ describe("getAgentServerBaseUrl", () => {
expect(getAgentServerBaseUrl()).toBe("https://agent.example.com");
});
});
+94
View File
@@ -0,0 +1,94 @@
import { spawn } from "node:child_process";
import { once } from "node:events";
import path from "node:path";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { buildSafeDevConfig } from "../../scripts/dev-safe.mjs";
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../..",
);
describe("buildSafeDevConfig", () => {
it("builds isolated default paths and ports", () => {
const cwd = "/workspace/project/agent-server-gui";
const config = buildSafeDevConfig(cwd, {});
expect(config.backendPort).toBe(18000);
expect(config.vscodePort).toBe(18001);
expect(config.backendBaseUrl).toBe("http://127.0.0.1:18000");
expect(config.backendHost).toBe("127.0.0.1:18000");
expect(config.workingDir).toBe(cwd);
expect(config.stateDir).toBe(
path.join(cwd, ".openhands-dev", "safe-dev-18000"),
);
expect(config.tmuxTmpDir).toBe(path.join(config.stateDir, "tmux"));
expect(config.conversationsPath).toBe(
path.join(config.stateDir, "conversations"),
);
expect(config.bashEventsDir).toBe(
path.join(config.stateDir, "bash_events"),
);
});
it("honors environment overrides", () => {
const cwd = "/workspace/project/agent-server-gui";
const config = buildSafeDevConfig(cwd, {
OH_GUI_SAFE_BACKEND_PORT: "19000",
OH_GUI_SAFE_VSCODE_PORT: "19010",
OH_GUI_SAFE_STATE_DIR: ".tmp/dev-safe",
VITE_WORKING_DIR: "/workspace/custom-repo",
});
expect(config.backendPort).toBe(19000);
expect(config.vscodePort).toBe(19010);
expect(config.backendBaseUrl).toBe("http://127.0.0.1:19000");
expect(config.backendHost).toBe("127.0.0.1:19000");
expect(config.stateDir).toBe(path.join(cwd, ".tmp", "dev-safe"));
expect(config.workingDir).toBe("/workspace/custom-repo");
});
});
describe("dev-safe CLI startup", () => {
it("exits promptly when agent-server is missing", async () => {
const child = spawn(process.execPath, ["scripts/dev-safe.mjs"], {
cwd: repoRoot,
env: {
...process.env,
PATH: "/usr/bin:/bin",
},
stdio: ["ignore", "pipe", "pipe"],
});
let output = "";
child.stdout.on("data", (chunk) => {
output += chunk.toString();
});
child.stderr.on("data", (chunk) => {
output += chunk.toString();
});
const exitResult = await Promise.race([
once(child, "exit").then(([code, signal]) => ({
code,
signal,
timedOut: false,
})),
delay(4_000).then(() => ({ code: null, signal: null, timedOut: true })),
]);
if (exitResult.timedOut) {
child.kill("SIGKILL");
}
expect(exitResult.timedOut).toBe(false);
expect(exitResult.code).toBe(1);
expect(output).toContain("Failed to start agent-server");
expect(output).toContain("spawn agent-server ENOENT");
});
});
+30
View File
@@ -0,0 +1,30 @@
import { execFile } from "node:child_process";
import process from "node:process";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
const execFileAsync = promisify(execFile);
describe("vite optimizeDeps", () => {
it("prebundles core client entry dependencies", async () => {
const { stdout } = await execFileAsync(
process.execPath,
[
"-e",
`import viteConfig from './vite.config.ts'; const config = await viteConfig({ mode: 'development', command: 'serve' }); console.log(JSON.stringify(config.optimizeDeps?.include ?? []));`,
],
{ cwd: process.cwd() },
);
const optimizedDeps = JSON.parse(stdout.trim()) as string[];
expect(optimizedDeps).toEqual(
expect.arrayContaining([
"react",
"react/jsx-runtime",
"react-dom/client",
"react-router/dom",
]),
);
});
});
+3 -1
View File
@@ -50,7 +50,9 @@
"zustand": "^5.0.10"
},
"scripts": {
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
"dev": "npm run dev:safe",
"dev:safe": "node scripts/dev-safe.mjs",
"dev:frontend": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
"dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=false react-router dev",
"dev:mock:saas": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=true react-router dev",
"build": "npm run make-i18n && react-router build",
+185
View File
@@ -0,0 +1,185 @@
import { spawn } from "node:child_process";
import { mkdirSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
const DEFAULT_BACKEND_PORT = 18000;
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
function parsePort(value, fallback) {
if (value == null || value === "") {
return fallback;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error(`Invalid port: ${value}`);
}
return parsed;
}
export function buildSafeDevConfig(cwd = process.cwd(), env = process.env) {
const backendPort = parsePort(env.OH_GUI_SAFE_BACKEND_PORT, DEFAULT_BACKEND_PORT);
const vscodePort = parsePort(env.OH_GUI_SAFE_VSCODE_PORT, backendPort + 1);
const stateDir = path.resolve(
cwd,
env.OH_GUI_SAFE_STATE_DIR || path.join(".openhands-dev", `safe-dev-${backendPort}`),
);
return {
cwd,
backendPort,
vscodePort,
stateDir,
tmuxTmpDir: path.join(stateDir, "tmux"),
conversationsPath: path.join(stateDir, "conversations"),
bashEventsDir: path.join(stateDir, "bash_events"),
backendBaseUrl: `http://127.0.0.1:${backendPort}`,
backendHost: `127.0.0.1:${backendPort}`,
workingDir: env.VITE_WORKING_DIR || cwd,
};
}
async function waitForServer(url, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
try {
const response = await fetch(url);
if (response.ok) {
return;
}
} catch {
// Keep polling until timeout.
}
await delay(500);
}
throw new Error(`Timed out waiting for agent-server at ${url}`);
}
function spawnProcess(command, args, options) {
const child = spawn(command, args, { stdio: "inherit", ...options });
child.once("error", (error) => {
if ((error && "code" in error && error.code === "ENOENT") || /ENOENT/.test(String(error))) {
console.error(`Failed to start ${command}. Make sure it is installed and on your PATH.`);
} else {
console.error(`Failed to start ${command}:`, error);
}
});
return child;
}
async function main() {
const config = buildSafeDevConfig();
for (const dir of [
config.stateDir,
config.tmuxTmpDir,
config.conversationsPath,
config.bashEventsDir,
]) {
mkdirSync(dir, { recursive: true });
}
console.log("Starting isolated agent-server + frontend dev stack...");
console.log(`- backend: ${config.backendBaseUrl}`);
console.log(`- vscode port: ${config.vscodePort}`);
console.log(`- working dir: ${config.workingDir}`);
console.log(`- isolated state dir: ${config.stateDir}`);
console.log("");
const backend = spawnProcess(
"agent-server",
["--host", "127.0.0.1", "--port", String(config.backendPort)],
{
cwd: config.cwd,
env: {
...process.env,
TMUX_TMPDIR: config.tmuxTmpDir,
OH_CONVERSATIONS_PATH: config.conversationsPath,
OH_BASH_EVENTS_DIR: config.bashEventsDir,
OH_VSCODE_PORT: String(config.vscodePort),
},
},
);
let shuttingDown = false;
let frontend = null;
const shutdown = (signal = "SIGTERM") => {
if (shuttingDown) {
return;
}
shuttingDown = true;
frontend?.kill(signal);
backend.kill(signal);
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
const backendErrored = new Promise((_, reject) => {
backend.once("error", (error) => reject(error));
});
const backendExited = new Promise((_, reject) => {
backend.once("exit", (code, signal) => {
if (!shuttingDown) {
reject(
new Error(
`agent-server exited before startup completed (code=${code ?? "null"}, signal=${signal ?? "null"})`,
),
);
}
});
});
try {
await Promise.race([
waitForServer(`${config.backendBaseUrl}/server_info`),
backendErrored,
backendExited,
]);
} catch (error) {
shutdown();
throw error;
}
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
frontend = spawnProcess(npmCommand, ["run", "dev:frontend"], {
cwd: config.cwd,
env: {
...process.env,
VITE_BACKEND_HOST: config.backendHost,
VITE_BACKEND_BASE_URL: config.backendBaseUrl,
VITE_WORKING_DIR: config.workingDir,
},
});
frontend.once("exit", (code) => {
shutdown();
process.exitCode = code ?? 0;
});
backend.once("exit", (code) => {
if (!shuttingDown) {
console.error(`agent-server exited unexpectedly with code ${code ?? 0}`);
shutdown();
process.exitCode = code ?? 1;
}
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});
}
+6
View File
@@ -33,6 +33,12 @@ export default defineConfig(({ mode }) => {
],
optimizeDeps: {
include: [
// Pre-bundle client entry dependencies so the first page load does not 504
// with Vite's "Outdated Optimize Dep" before hydration finishes.
"react",
"react/jsx-runtime",
"react-dom/client",
"react-router/dom",
// Pre-bundle ALL dependencies to prevent runtime optimization and page reloads
// These are discovered during initial app load:
"posthog-js",