mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:38:34 +08:00
Warn on incompatible agent server version (#18)
* Warn on incompatible agent server version Co-authored-by: openhands <openhands@all-hands.dev> * Use server version for compatibility guard Co-authored-by: openhands <openhands@all-hands.dev> * Tighten compatibility guard typing Co-authored-by: openhands <openhands@all-hands.dev> * Update * Warn when agent server is unavailable Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
committed by
GitHub
co-authored by
openhands
parent
66542e4976
commit
3f84ba07a6
@@ -16,6 +16,7 @@
|
||||
- Verification command: `npm run typecheck && npm run build`.
|
||||
- `@openhands/typescript-client` is currently consumed via a vendored local file dependency at `vendor/openhands-typescript-client` because anonymous/public GitHub Packages install was not usable in this environment. The vendored package is pinned to `v0.1.2`, keeps built `dist/` output committed, and exposes extra subpath exports for `client/http-client`, `events/remote-events-list`, and `workspace/remote-workspace`.
|
||||
- Shared TypeScript-client adapters live in `src/api/typescript-client.ts`; prefer those helpers for agent-server-backed REST/workspace/event/VS Code calls before falling back to `open-hands-axios`.
|
||||
- In this repo state, `src/api/typescript-client.ts` imports the vendored TypeScript client directly from `vendor/openhands-typescript-client/src/*` rather than the package subpath exports, because the file dependency does not currently ship the expected built `dist/` subpath files in this environment.
|
||||
- Root `tsconfig.json` excludes `vendor/openhands-typescript-client` so the frontend typecheck only covers the app code.
|
||||
|
||||
- `npm run dev:mock` needs MSW handlers for the direct agent-server routes used by the adapted frontend, not the original OpenHands mock paths. Key routes that must stay covered are:
|
||||
@@ -23,6 +24,7 @@
|
||||
- settings schemas: `/api/settings/agent-schema`, `/api/settings/conversation-schema`
|
||||
- conversation browsing/loading: `/api/conversations/search`, `/api/conversations?ids=...`, `/api/conversations/:id`, `/api/conversations/:id/events/*`
|
||||
- runtime git panels: `/api/git/changes`, `/api/git/diff`
|
||||
- Frontend compatibility guard: `OptionService.getConfig()` now uses `/server_info.version` to block unsupported agent-server versions before the app loads. Git history in `software-agent-sdk` shows `/api/settings/agent-schema` and `/api/settings/conversation-schema` first shipped in tag `v1.17.0`, so the GUI currently treats `< 1.17.0` (or unknown/unparseable versions) as incompatible, `useConfig` stops retrying that case, and `src/root.tsx` renders a blocking unsupported-version notice on every route.
|
||||
- Useful regression tests for mock mode live in `__tests__/api/option-service.test.ts`, `__tests__/api/mock-conversation-handlers.test.ts`, and `__tests__/api/mock-settings-handlers.test.ts`.
|
||||
- Browser-verified mock-mode tour artifact was generated at `artifacts/frontend-tour.gif`.
|
||||
- Live `agent_server` compatibility quirks discovered during browser verification:
|
||||
@@ -41,12 +43,9 @@
|
||||
- `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`.
|
||||
- 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`.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import {
|
||||
AgentServerIncompatibilityError,
|
||||
AgentServerUnavailableError,
|
||||
MINIMUM_SUPPORTED_AGENT_SERVER_VERSION,
|
||||
} from "#/api/agent-server-compatibility";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { server } from "#/mocks/node";
|
||||
|
||||
describe("OptionService", () => {
|
||||
it("returns config in mock mode without a live backend", async () => {
|
||||
@@ -10,6 +17,55 @@ describe("OptionService", () => {
|
||||
expect(config.updated_at).toBeTruthy();
|
||||
});
|
||||
|
||||
it("throws a compatibility error when the agent server version is below the supported minimum", async () => {
|
||||
server.use(
|
||||
http.get("/server_info", () =>
|
||||
HttpResponse.json({ uptime: 0, idle_time: 0, version: "1.16.1" }),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(OptionService.getConfig()).rejects.toMatchObject({
|
||||
name: AgentServerIncompatibilityError.name,
|
||||
serverVersion: "1.16.1",
|
||||
message: expect.stringContaining(MINIMUM_SUPPORTED_AGENT_SERVER_VERSION),
|
||||
});
|
||||
});
|
||||
|
||||
it("throws an unavailable error when the agent server cannot be reached", async () => {
|
||||
server.use(http.get("/server_info", () => HttpResponse.error()));
|
||||
|
||||
await expect(OptionService.getConfig()).rejects.toMatchObject({
|
||||
name: AgentServerUnavailableError.name,
|
||||
message: expect.stringContaining("Agent server not found"),
|
||||
details: expect.stringContaining("Request failed"),
|
||||
});
|
||||
});
|
||||
|
||||
it("uses only server version metadata for compatibility checks", async () => {
|
||||
server.use(
|
||||
http.get("/server_info", () =>
|
||||
HttpResponse.json({
|
||||
uptime: 0,
|
||||
idle_time: 0,
|
||||
version: MINIMUM_SUPPORTED_AGENT_SERVER_VERSION,
|
||||
}),
|
||||
),
|
||||
http.get("/api/settings/agent-schema", () =>
|
||||
HttpResponse.json({ error: "missing" }, { status: 404 }),
|
||||
),
|
||||
http.get("/api/settings/conversation-schema", () =>
|
||||
HttpResponse.json({ error: "missing" }, { status: 404 }),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(OptionService.getConfig()).resolves.toMatchObject({
|
||||
app_mode: "oss",
|
||||
feature_flags: expect.objectContaining({
|
||||
deployment_mode: "self_hosted",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns models from mocked LLM endpoints", async () => {
|
||||
const models = await OptionService.getModels();
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { MINIMUM_SUPPORTED_AGENT_SERVER_VERSION } from "#/api/agent-server-compatibility";
|
||||
import App from "#/root";
|
||||
import { server } from "#/mocks/node";
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: App,
|
||||
path: "/",
|
||||
children: [
|
||||
{
|
||||
Component: () => <div data-testid="app-outlet">app outlet</div>,
|
||||
path: "/",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const renderApp = (initialEntries: string[] = ["/"]) =>
|
||||
render(<RouterStub initialEntries={initialEntries} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider
|
||||
client={
|
||||
new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
describe("App root compatibility guard", () => {
|
||||
it("blocks the app on any page when the connected agent server version is too old", async () => {
|
||||
server.use(
|
||||
http.get("/server_info", () =>
|
||||
HttpResponse.json({ uptime: 0, idle_time: 0, version: "1.16.1" }),
|
||||
),
|
||||
);
|
||||
|
||||
renderApp(["/"]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId("agent-server-incompatibility-warning"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(/unsupported agent server version/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/1\.16\.1/)).toHaveLength(2);
|
||||
expect(
|
||||
screen.getByText(
|
||||
new RegExp(`${MINIMUM_SUPPORTED_AGENT_SERVER_VERSION} or newer`),
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("app-outlet")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fails fast with an agent server not found warning when the backend is unreachable", async () => {
|
||||
let serverInfoRequests = 0;
|
||||
|
||||
server.use(
|
||||
http.get("/server_info", () => {
|
||||
serverInfoRequests += 1;
|
||||
return HttpResponse.error();
|
||||
}),
|
||||
);
|
||||
|
||||
renderApp(["/"]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId("agent-server-unavailable-warning"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: /agent server not found/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(serverInfoRequests).toBe(1);
|
||||
expect(screen.queryByTestId("app-outlet")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the routed page when the agent server is compatible", async () => {
|
||||
renderApp(["/"]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("app-outlet")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("agent-server-incompatibility-warning"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Generated
+12
-5818
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -11,7 +11,7 @@
|
||||
"@heroui/react": "2.8.7",
|
||||
"@microlink/react-json-view": "^1.27.1",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@openhands/typescript-client": "file:vendor/openhands-typescript-client",
|
||||
"@openhands/typescript-client": "github:OpenHands/typescript-client#4716d2e",
|
||||
"@react-router/node": "^7.12.0",
|
||||
"@react-router/serve": "^7.12.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createServerClient, type ServerInfo } from "#/api/typescript-client";
|
||||
import { HttpError } from "@openhands/typescript-client/client/http-client";
|
||||
|
||||
export const MINIMUM_SUPPORTED_AGENT_SERVER_VERSION = "1.17.0";
|
||||
const AGENT_SERVER_INFO_TIMEOUT_MS = 5000;
|
||||
|
||||
const SEMVER_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
||||
|
||||
const getServerVersion = (serverInfo: ServerInfo): string => serverInfo.version;
|
||||
|
||||
const parseSemver = (
|
||||
version: string | null,
|
||||
): [number, number, number] | null => {
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = version.match(SEMVER_PATTERN);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match.slice(1, 4).map(Number) as [number, number, number];
|
||||
};
|
||||
|
||||
const isSupportedAgentServerVersion = (serverVersion: string | null) => {
|
||||
const parsedVersion = parseSemver(serverVersion);
|
||||
const minimumVersion = parseSemver(MINIMUM_SUPPORTED_AGENT_SERVER_VERSION);
|
||||
|
||||
if (!parsedVersion || !minimumVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < minimumVersion.length; index += 1) {
|
||||
if (parsedVersion[index] > minimumVersion[index]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parsedVersion[index] < minimumVersion[index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const buildCompatibilityMessage = (serverVersion: string | null) => {
|
||||
const versionMessage = serverVersion
|
||||
? `Connected agent server version ${serverVersion} is not compatible with this frontend.`
|
||||
: "The connected agent server version could not be determined.";
|
||||
|
||||
return `${versionMessage} This frontend requires agent server version ${MINIMUM_SUPPORTED_AGENT_SERVER_VERSION} or newer. Upgrade the agent server and reload the page.`;
|
||||
};
|
||||
|
||||
export class AgentServerIncompatibilityError extends Error {
|
||||
readonly serverVersion: string | null;
|
||||
|
||||
constructor(serverVersion: string | null) {
|
||||
super(buildCompatibilityMessage(serverVersion));
|
||||
this.name = "AgentServerIncompatibilityError";
|
||||
this.serverVersion = serverVersion;
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentServerUnavailableError extends Error {
|
||||
readonly details: string | null;
|
||||
|
||||
constructor(details?: string | null) {
|
||||
super(
|
||||
"Agent server not found. Could not connect to the configured agent server. Start a compatible agent server and reload the page.",
|
||||
);
|
||||
this.name = "AgentServerUnavailableError";
|
||||
this.details = details ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
export const isAgentServerIncompatibilityError = (
|
||||
error: unknown,
|
||||
): error is AgentServerIncompatibilityError =>
|
||||
error instanceof AgentServerIncompatibilityError ||
|
||||
(typeof error === "object" &&
|
||||
error !== null &&
|
||||
"name" in error &&
|
||||
error.name === "AgentServerIncompatibilityError");
|
||||
|
||||
export const isAgentServerUnavailableError = (
|
||||
error: unknown,
|
||||
): error is AgentServerUnavailableError =>
|
||||
error instanceof AgentServerUnavailableError ||
|
||||
(typeof error === "object" &&
|
||||
error !== null &&
|
||||
"name" in error &&
|
||||
error.name === "AgentServerUnavailableError");
|
||||
|
||||
export async function ensureCompatibleAgentServer() {
|
||||
let serverInfo: ServerInfo;
|
||||
|
||||
try {
|
||||
serverInfo = await createServerClient({
|
||||
timeout: AGENT_SERVER_INFO_TIMEOUT_MS,
|
||||
}).getServerInfo();
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const details = error instanceof Error ? error.message : null;
|
||||
throw new AgentServerUnavailableError(details);
|
||||
}
|
||||
|
||||
const serverVersion = getServerVersion(serverInfo);
|
||||
|
||||
if (!isSupportedAgentServerVersion(serverVersion)) {
|
||||
throw new AgentServerIncompatibilityError(serverVersion);
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLlmMetadataClient, createServerClient } from "../typescript-client";
|
||||
import { ensureCompatibleAgentServer } from "../agent-server-compatibility";
|
||||
import { createLlmMetadataClient } from "../typescript-client";
|
||||
import { ModelsResponse, WebClientConfig } from "./option.types";
|
||||
|
||||
class OptionService {
|
||||
@@ -30,7 +31,7 @@ class OptionService {
|
||||
}
|
||||
|
||||
static async getConfig(): Promise<WebClientConfig> {
|
||||
await createServerClient().getServerInfo();
|
||||
await ensureCompatibleAgentServer();
|
||||
|
||||
return {
|
||||
app_mode: "oss",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SkillsClient,
|
||||
VSCodeClient,
|
||||
} from "@openhands/typescript-client/clients";
|
||||
export type { ServerInfo } from "@openhands/typescript-client";
|
||||
import { HttpClient } from "@openhands/typescript-client/client/http-client";
|
||||
import { RemoteEventsList } from "@openhands/typescript-client/events/remote-events-list";
|
||||
import { RemoteWorkspace } from "@openhands/typescript-client/workspace/remote-workspace";
|
||||
@@ -21,6 +22,7 @@ interface TypeScriptClientOverrides {
|
||||
sessionApiKey?: string | null;
|
||||
workingDir?: string;
|
||||
conversationUrl?: string | null;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
interface ResolvedClientOptions {
|
||||
@@ -55,7 +57,11 @@ export function createServerClient(
|
||||
overrides?: TypeScriptClientOverrides,
|
||||
): ServerClient {
|
||||
const { host, apiKey } = resolveClientOptions(overrides);
|
||||
return new ServerClient({ host, ...(apiKey ? { apiKey } : {}) });
|
||||
return new ServerClient({
|
||||
host,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
...(overrides?.timeout ? { timeout: overrides.timeout } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function createLlmMetadataClient(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
isAgentServerIncompatibilityError,
|
||||
isAgentServerUnavailableError,
|
||||
} from "#/api/agent-server-compatibility";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { useIsOnIntermediatePage } from "#/hooks/use-is-on-intermediate-page";
|
||||
import { QUERY_KEYS, CONFIG_CACHE_OPTIONS } from "./query-keys";
|
||||
@@ -13,6 +17,11 @@ export const useConfig = (options?: UseConfigOptions) => {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.WEB_CLIENT_CONFIG,
|
||||
queryFn: OptionService.getConfig,
|
||||
retry: (failureCount, error) =>
|
||||
!isAgentServerIncompatibilityError(error) &&
|
||||
!isAgentServerUnavailableError(error) &&
|
||||
failureCount < 3,
|
||||
meta: { disableToast: true },
|
||||
...CONFIG_CACHE_OPTIONS,
|
||||
enabled: options?.enabled ?? !isOnIntermediatePage,
|
||||
});
|
||||
|
||||
@@ -345,6 +345,9 @@ const MOCK_VERIFIED_MODELS_BY_PROVIDER = MOCK_MODELS.reduce<
|
||||
export const SETTINGS_HANDLERS = [
|
||||
http.get("/server_info", async () =>
|
||||
HttpResponse.json({
|
||||
uptime: 0,
|
||||
idle_time: 0,
|
||||
version: "1.18.1",
|
||||
agents: ["CodeActAgent"],
|
||||
default_agent: "CodeActAgent",
|
||||
models: MOCK_MODELS,
|
||||
|
||||
@@ -10,6 +10,13 @@ import "./tailwind.css";
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import {
|
||||
AgentServerIncompatibilityError,
|
||||
AgentServerUnavailableError,
|
||||
isAgentServerIncompatibilityError,
|
||||
isAgentServerUnavailableError,
|
||||
} from "#/api/agent-server-compatibility";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useInvitation } from "#/hooks/use-invitation";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
@@ -32,15 +39,93 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AgentServerNotice({
|
||||
testId,
|
||||
title,
|
||||
message,
|
||||
children,
|
||||
}: {
|
||||
testId: string;
|
||||
title: string;
|
||||
message: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<main className="min-h-screen flex items-center justify-center bg-base p-6 text-white">
|
||||
<div
|
||||
data-testid={testId}
|
||||
className="w-full max-w-2xl rounded-2xl border border-danger/30 bg-neutral-900/80 p-8 shadow-2xl"
|
||||
>
|
||||
<p className="text-sm font-medium uppercase tracking-[0.24em] text-danger">
|
||||
Connection blocked
|
||||
</p>
|
||||
<h1 className="mt-3 text-3xl font-semibold text-white">{title}</h1>
|
||||
<p className="mt-4 text-base leading-7 text-neutral-200">{message}</p>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function UnsupportedAgentServerNotice({
|
||||
error,
|
||||
}: {
|
||||
error: AgentServerIncompatibilityError;
|
||||
}) {
|
||||
return (
|
||||
<AgentServerNotice
|
||||
testId="agent-server-incompatibility-warning"
|
||||
title="Unsupported agent server version"
|
||||
message={error.message}
|
||||
>
|
||||
{error.serverVersion && (
|
||||
<p className="mt-4 text-sm text-neutral-400">
|
||||
Detected version: <code>{error.serverVersion}</code>
|
||||
</p>
|
||||
)}
|
||||
</AgentServerNotice>
|
||||
);
|
||||
}
|
||||
|
||||
function MissingAgentServerNotice({
|
||||
error,
|
||||
}: {
|
||||
error: AgentServerUnavailableError;
|
||||
}) {
|
||||
return (
|
||||
<AgentServerNotice
|
||||
testId="agent-server-unavailable-warning"
|
||||
title="Agent server not found"
|
||||
message={error.message}
|
||||
>
|
||||
{error.details && (
|
||||
<p className="mt-4 text-sm text-neutral-400">
|
||||
Details: {error.details}
|
||||
</p>
|
||||
)}
|
||||
</AgentServerNotice>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = () => [
|
||||
{ title: "OpenHands" },
|
||||
{ name: "description", content: "Let's Start Building!" },
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
const config = useConfig({ enabled: true });
|
||||
|
||||
// Handle invitation token cleanup when invitation flow completes
|
||||
// This runs on all pages to catch redirects from auth callback
|
||||
useInvitation();
|
||||
|
||||
if (isAgentServerUnavailableError(config.error)) {
|
||||
return <MissingAgentServerNotice error={config.error} />;
|
||||
}
|
||||
|
||||
if (isAgentServerIncompatibilityError(config.error)) {
|
||||
return <UnsupportedAgentServerNotice error={config.error} />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
+5
-1
@@ -120,7 +120,11 @@ export default defineConfig(({ mode }) => {
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: ["vitest.setup.ts"],
|
||||
exclude: [...configDefaults.exclude, "tests"],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
"tests",
|
||||
"vendor/openhands-typescript-client/**",
|
||||
],
|
||||
coverage: {
|
||||
reporter: ["text", "json", "html", "lcov", "text-summary"],
|
||||
reportsDirectory: "coverage",
|
||||
|
||||
Reference in New Issue
Block a user