From 48db135701b181463646aeb46b5e35a10d12efb7 Mon Sep 17 00:00:00 2001
From: diegosouzapw
Date: Sat, 20 Jun 2026 19:15:09 -0300
Subject: [PATCH] fix(dashboard): surface manual config CTA when Claude CLI
auto-detect fails
When OmniRoute is deployed on a remote server, the Claude tool card's CLI
auto-detect runs on the server (where Claude CLI is not installed) and the
warning panel was hiding the "Manual Config" button. Users with Claude
installed on their local machine had no way to copy the settings.json
snippet from the dashboard.
Always surface the "Manual Config" button alongside "How to Install"
in the detection-failure panel. The button opens the same ManualConfigModal
already used in the happy path. The "How to Install" guide stays put for
users intending a local install. Added vitest regression covering both the
button presence and the modal open behavior when detection returns
{ installed: false }.
Co-authored-by: anuragg-saxenaa
Inspired-by: https://github.com/decolua/9router/pull/602
---
CHANGELOG.md | 1 +
.../cli-code/components/ClaudeToolCard.tsx | 38 +++-
...deToolCard-manual-config-fallback.test.tsx | 176 ++++++++++++++++++
3 files changed, 205 insertions(+), 10 deletions(-)
create mode 100644 tests/unit/ui/ClaudeToolCard-manual-config-fallback.test.tsx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d63d6da93e..ae256d01cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ _In development — bullets added per PR; finalized at release._
- **fix(embeddings):** forward output dimensions to Gemini for consistent embedding dims. (thanks @nguyenha935)
- **fix(translator):** sanitize Read tool args from non-Anthropic models to prevent retry loops. (thanks @GodrezJr2)
- **fix(usage):** reuse Gemini CLI project ID for quota checks (avoid re-discovery). (thanks @Delcado19)
+- **fix(dashboard):** surface manual config CTA when Claude CLI detection fails (remote deployments). (thanks @anuragg-saxenaa)
- **fix(combo): round-robin members fail over faster under concurrency saturation via a configurable queue depth** — when a round-robin combo member was saturated, requests sat in the per-model semaphore's **unbounded** queue and only failed over to the next member after the full `queueTimeoutMs` (default 30s) elapsed — so a burst of agentic requests deep-queued one hot member instead of spilling to healthy ones. The per-model semaphore now accepts a bounded queue depth and emits `SEMAPHORE_QUEUE_FULL` once it is full (the round-robin loop already cascades on that code), so a configured low depth fails over immediately. A new `queueDepth` combo-config knob (global default / provider override / per-combo, default **20** for backward compatibility; **0** = never queue → fail over now) is exposed in Settings → Combo Defaults. ([#3872](https://github.com/diegosouzapw/OmniRoute/issues/3872) — thanks @KooshaPari)
- **fix(pricing): align Claude Code (`cc`) pricing with current Anthropic per-MTok rates** — the `cc` provider block in the default pricing table had stale numbers across every Claude 4.x family entry — most visibly, `claude-opus-4-5-20251101` was billed at the deprecated Opus 4.1 rate (`input $15` / `output $75`), and `claude-haiku-4-5-20251001` was at half the current Haiku 4.5 rate. The `cached` (cache hit) and `cache_creation` (5-minute cache write) multipliers were also off across Opus 4.6/4.7/4.8, Sonnet 4.5/4.6, Haiku 4.5, and Fable 5. All eight entries now match the rates Anthropic publishes (input, 5m cache write at 1.25x input, cache hit at 0.1x input, output; reasoning billed at the output rate), so cost accounting on the dashboard and per-request usage events stop under- or over-reporting Claude Code spend. (thanks @chulanpro5)
diff --git a/src/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard.tsx b/src/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard.tsx
index 8d85109303..832ff024b2 100644
--- a/src/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard.tsx
+++ b/src/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard.tsx
@@ -335,16 +335,34 @@ export default function ClaudeToolCard({
: t("installCliPrompt", { tool: "Claude" })}
-
+
+ {/*
+ 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: #589.
+ */}
+
+
+
{showInstallGuide && (
diff --git a/tests/unit/ui/ClaudeToolCard-manual-config-fallback.test.tsx b/tests/unit/ui/ClaudeToolCard-manual-config-fallback.test.tsx
new file mode 100644
index 0000000000..c404218679
--- /dev/null
+++ b/tests/unit/ui/ClaudeToolCard-manual-config-fallback.test.tsx
@@ -0,0 +1,176 @@
+// @vitest-environment jsdom
+//
+// Regression test: when the Claude 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#589).
+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}.",
+ howToInstall: "How to Install",
+ hide: "Hide",
+ manualConfig: "Manual Config",
+ installationGuide: "Installation Guide",
+ platforms: "Platforms",
+ afterInstallationRun: "After installation run",
+ toVerify: "to verify.",
+ checkingCli: "Checking {tool}...",
+ claudeManualConfiguration: "Claude Manual Configuration",
+ };
+ 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("@/shared/components/ProviderIcon", () => ({
+ default: () => ,
+}));
+
+vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({
+ default: () => ,
+}));
+
+vi.mock("@/shared/services/claudeCliConfig", () => ({
+ getStoredClaudeAuthValue: () => null,
+ normalizeClaudeBaseUrl: (u: string) => u,
+}));
+
+// 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 }) =>