+ {/*
+ Always surface Manual Config even when the CLI is not
+ detected locally — typical of remote OmniRoute
+ deployments where the CLI lives on the user's machine,
+ not on the server. Upstream report: #579.
+ */}
+
)}
diff --git a/tests/unit/ui/OpenClawToolCard-manual-config-fallback.test.tsx b/tests/unit/ui/OpenClawToolCard-manual-config-fallback.test.tsx
new file mode 100644
index 0000000000..4c5ea89ef3
--- /dev/null
+++ b/tests/unit/ui/OpenClawToolCard-manual-config-fallback.test.tsx
@@ -0,0 +1,160 @@
+// @vitest-environment jsdom
+//
+// Regression test: when the Open Claw CLI is not detected locally (typical of
+// remote OmniRoute deployments where the CLI lives on the user's laptop, not
+// on the server), the card must still surface a "Manual Config" button so the
+// user can copy the settings.json snippet and paste it into the CLI on their
+// local machine. Before this fix the Manual Config button only rendered when
+// `cliReady === true`, which made the card useless for remote deployments
+// (upstream report: decolua/9router#579, port of decolua/9router#615).
+import React from "react";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+// ── Mocks ─────────────────────────────────────────────────────────────────────
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string, values?: Record) => {
+ const messages: Record = {
+ cliNotInstalled: "{tool} CLI not detected locally",
+ cliNotRunnable: "{tool} CLI installed but not runnable",
+ installCliPrompt:
+ "Manual configuration is still available if OmniRoute is deployed on a remote server.",
+ cliFoundFailedHealthcheck: "{tool} CLI was found but failed runtime healthcheck{reason}.",
+ manualConfig: "Manual Config",
+ checkingCli: "Checking {tool}...",
+ openClawManualConfiguration: "Open Claw Manual Configuration",
+ "toolDescriptions.openclaw": "Open Claw CLI",
+ };
+ const raw = messages[key] ?? key;
+ if (!values) return raw;
+ return Object.entries(values).reduce(
+ (acc, [k, v]) => acc.replaceAll(`{${k}}`, String(v ?? "")),
+ raw
+ );
+ },
+ useLocale: () => "en",
+}));
+
+vi.mock("next/image", () => ({
+ default: () => ,
+}));
+
+vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({
+ default: () => ,
+}));
+
+// Surface ManualConfigModal as a marker so we can assert it gets rendered with
+// isOpen=true after clicking the new Manual Config button.
+vi.mock("@/shared/components", async () => {
+ const React = await import("react");
+ return {
+ Card: ({ children }: { children: React.ReactNode }) =>