+ {/* Back navigation */}
+
+
+ arrow_back
+ {category === "code" ? "CLI Code" : "CLI Agents"}
+
+ /
+ {tool.name}
+
+
+ {/* Tool header */}
+
+ {tool.vendor && (
+
+ {tool.vendor}
+
+ )}
+
+ {category}
+
+ {tool.baseUrlSupport && tool.baseUrlSupport !== "none" && (
+
+ link
+ {tool.baseUrlSupport === "full" ? "Full base URL" : "Partial base URL"}
+
+ )}
+
+
+ {/* Specialized card — always expanded */}
+ {loading ? (
+
+ ) : (
+ renderCard()
+ )}
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/customCliConfig.ts b/src/app/(dashboard)/dashboard/cli-code/components/customCliConfig.ts
similarity index 100%
rename from src/app/(dashboard)/dashboard/cli-tools/components/customCliConfig.ts
rename to src/app/(dashboard)/dashboard/cli-code/components/customCliConfig.ts
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx b/src/app/(dashboard)/dashboard/cli-code/components/index.tsx
similarity index 100%
rename from src/app/(dashboard)/dashboard/cli-tools/components/index.tsx
rename to src/app/(dashboard)/dashboard/cli-code/components/index.tsx
diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx
deleted file mode 100644
index fc9a6c6586..0000000000
--- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx
+++ /dev/null
@@ -1,513 +0,0 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import { Card, CardSkeleton, SegmentedControl } from "@/shared/components";
-import { CLI_TOOLS } from "@/shared/constants/cliTools";
-import {
- PROVIDER_MODELS,
- getModelsByProviderId,
- PROVIDER_ID_TO_ALIAS,
-} from "@/shared/constants/models";
-import {
- ClaudeToolCard,
- CodexToolCard,
- DroidToolCard,
- OpenClawToolCard,
- ClineToolCard,
- KiloToolCard,
- DefaultToolCard,
- AntigravityToolCard,
- CopilotToolCard,
- CustomCliCard,
- HermesAgentToolCard,
-} from "./components";
-import { useTranslations } from "next-intl";
-import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
-
-const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
-const AUTO_CONFIGURED_TOOL_IDS = new Set([
- "claude",
- "codex",
- "droid",
- "openclaw",
- "cline",
- "kilo",
- "copilot",
- "hermes-agent",
-]);
-const GUIDED_TOOL_IDS = new Set([
- "cursor",
- "windsurf",
- "continue",
- "opencode",
- "hermes",
- "amp",
- "qwen",
-]);
-const MITM_TOOL_IDS = new Set(["antigravity", "kiro"]);
-const CUSTOM_TOOL_IDS = new Set(["custom"]);
-
-export default function CLIToolsPageClient({ machineId: _machineId }) {
- const t = useTranslations("cliTools");
- const [connections, setConnections] = useState([]);
- const [loading, setLoading] = useState(true);
- const [expandedTool, setExpandedTool] = useState(null);
- const [modelMappings, setModelMappings] = useState({});
- const [cloudEnabled, setCloudEnabled] = useState(false);
- const [apiKeys, setApiKeys] = useState([]);
- const [toolStatuses, setToolStatuses] = useState({});
- const [statusesLoaded, setStatusesLoaded] = useState(false);
- const [dynamicModels, setDynamicModels] = useState([]);
- const [activeCategory, setActiveCategory] = useState("auto");
- const translateOrFallback = useCallback(
- (key, fallback, values = undefined) => {
- try {
- const translated = t(key, values);
- return translated === key || translated === `cliTools.${key}` ? fallback : translated;
- } catch {
- return fallback;
- }
- },
- [t]
- );
-
- useEffect(() => {
- fetchConnections();
- loadCloudSettings();
- fetchApiKeys();
- fetchToolStatuses();
- fetchDynamicModels();
- }, []);
-
- const loadCloudSettings = async () => {
- try {
- const res = await fetch("/api/settings");
- if (res.ok) {
- const data = await res.json();
- setCloudEnabled(data.cloudEnabled || false);
- }
- } catch (error) {
- console.log("Error loading cloud settings:", error);
- }
- };
-
- const fetchApiKeys = async () => {
- try {
- const res = await fetch("/api/cli-tools/keys");
- if (res.ok) {
- const data = await res.json();
- setApiKeys(data.keys || []);
- }
- } catch (error) {
- console.log("Error fetching API keys:", error);
- }
- };
-
- const fetchToolStatuses = async () => {
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 8000); // 8s client timeout
- const res = await fetch("/api/cli-tools/status", { signal: controller.signal });
- clearTimeout(timeoutId);
- if (res.ok) {
- const data = await res.json();
- setToolStatuses(data || {});
- }
- } catch (error) {
- // Timeout or network error — proceed without statuses
- console.log("CLI tool status check timed out or failed:", error);
- } finally {
- setStatusesLoaded(true);
- }
- };
-
- const fetchConnections = async () => {
- try {
- const res = await fetch("/api/providers");
- const data = await res.json();
- if (res.ok) {
- setConnections(data.connections || []);
- }
- } catch (error) {
- console.log("Error fetching connections:", error);
- } finally {
- setLoading(false);
- }
- };
-
- const fetchDynamicModels = async () => {
- try {
- const res = await fetch("/v1/models");
- if (res.ok) {
- const data = await res.json();
- setDynamicModels(data?.data || []);
- }
- } catch (error) {
- console.log("Error fetching dynamic models:", error);
- }
- };
-
- const getActiveProviders = () => {
- return connections.filter((c) => c.isActive !== false);
- };
-
- const getAllAvailableModels = () => {
- const activeProviders = getActiveProviders();
- const models = [];
- const seenModels = new Set();
-
- // First: add static models from the constants
- activeProviders.forEach((conn) => {
- const alias = PROVIDER_ID_TO_ALIAS[conn.provider] || conn.provider;
- const providerModels = getModelsByProviderId(conn.provider);
- providerModels.forEach((m) => {
- const modelValue = `${alias}/${m.id}`;
- if (!seenModels.has(modelValue)) {
- seenModels.add(modelValue);
- models.push({
- value: modelValue,
- label: `${alias}/${m.id}`,
- provider: conn.provider,
- alias: alias,
- connectionName: conn.name,
- modelId: m.id,
- });
- }
- });
- });
-
- // Second: add dynamic models from /v1/models (fills gaps for Kiro, OpenCode, custom providers)
- const activeProviderIds = new Set(activeProviders.map((c) => c.provider));
- const activeAliases = new Set(
- activeProviders.map((c) => PROVIDER_ID_TO_ALIAS[c.provider] || c.provider)
- );
- dynamicModels.forEach((dm) => {
- const rawId = dm?.id ?? dm;
- const modelId = typeof rawId === "string" ? rawId : "";
- if (!modelId || seenModels.has(modelId)) return;
- // Parse alias/model format
- const slashIdx = modelId.indexOf("/");
- if (slashIdx === -1) return;
- const alias = modelId.substring(0, slashIdx);
- const bareModel = modelId.substring(slashIdx + 1);
- if (!activeAliases.has(alias) && !activeProviderIds.has(alias)) return;
- seenModels.add(modelId);
- models.push({
- value: modelId,
- label: modelId,
- provider: alias,
- alias: alias,
- connectionName: "",
- modelId: bareModel,
- });
- });
-
- return models;
- };
-
- const handleModelMappingChange = useCallback((toolId, modelAlias, targetModel) => {
- setModelMappings((prev) => {
- // Prevent unnecessary updates if value hasn't changed
- if (prev[toolId]?.[modelAlias] === targetModel) {
- return prev;
- }
- return {
- ...prev,
- [toolId]: {
- ...prev[toolId],
- [modelAlias]: targetModel,
- },
- };
- });
- }, []);
-
- const getBaseUrl = () => {
- if (cloudEnabled && CLOUD_URL) {
- return CLOUD_URL;
- }
- // Use window.location.origin directly — works correctly in Docker/reverse-proxy
- // Per @alpgul feedback: don't use baseUrl prop (has port duplication issues)
- if (typeof window !== "undefined") {
- return window.location.origin;
- }
- return DEFAULT_DISPLAY_BASE_URL;
- };
-
- if (loading || !statusesLoaded) {
- return (
-