From a3153d893a2d1e094b5a5cfbb47d737d36d1066e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 28 Feb 2026 11:31:55 -0300 Subject: [PATCH] feat: GitHub Copilot config generator for CLI Tools (#142) Adds a Copilot configuration generator to the CLI Tools dashboard page. Users can select models and generate the chatLanguageModels.json config block for VS Code GitHub Copilot with the Azure vendor pattern. Features: - Bulk model selection from /v1/models (includes combos, custom, aliased) - Search/filter for large model lists - Configurable maxInputTokens, maxOutputTokens, toolCalling, vision - One-click copy to clipboard - Persistent model selection via localStorage - Version compatibility warning (VS Code >= 1.109, Copilot >= v0.37) Feedback from @alpgul applied: - Use /v1/models instead of /api/models/alias (includes combo definitions) - Use window.location.origin for URL (no port duplication in Docker) Also: added electron/dist-electron/ to .gitignore (build artifact) --- .gitignore | 1 + .../cli-tools/CLIToolsPageClient.tsx | 11 + .../cli-tools/components/CopilotToolCard.tsx | 423 ++++++++++++++++++ .../dashboard/cli-tools/components/index.tsx | 1 + src/shared/constants/cliTools.ts | 8 + 5 files changed, 444 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.tsx diff --git a/.gitignore b/.gitignore index b3f77d7c15..b6da6bdb51 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ app.__qa_backup/ # Electron (subproject dependency lock) electron/package-lock.json icon.iconset/ +electron/dist-electron/ diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx index 660b174142..15df54eb18 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx @@ -17,6 +17,7 @@ import { KiloToolCard, DefaultToolCard, AntigravityToolCard, + CopilotToolCard, } from "./components"; import { useTranslations } from "next-intl"; @@ -263,6 +264,16 @@ export default function CLIToolsPageClient({ machineId }) { cloudEnabled={cloudEnabled} /> ); + case "copilot": + return ( + + ); default: return ( (null); + const [selectedModels, setSelectedModels] = useState>(() => { + if (typeof window === "undefined") return new Set(); + try { + const saved = localStorage.getItem("omniroute-copilot-selected-models"); + return saved ? new Set(JSON.parse(saved)) : new Set(); + } catch { + return new Set(); + } + }); + const [selectedApiKey, setSelectedApiKey] = useState(() => { + if (typeof window !== "undefined") { + const savedKey = localStorage.getItem("omniroute-cli-key-copilot"); + if (savedKey && apiKeys?.some((k: any) => k.key === savedKey)) return savedKey; + } + return apiKeys?.length > 0 ? apiKeys[0].key : ""; + }); + const [maxInputTokens, setMaxInputTokens] = useState(128000); + const [maxOutputTokens, setMaxOutputTokens] = useState(16000); + const [toolCalling, setToolCalling] = useState(true); + const [vision, setVision] = useState(false); + const [allModels, setAllModels] = useState>([]); + const [modelsLoaded, setModelsLoaded] = useState(false); + const [searchFilter, setSearchFilter] = useState(""); + + // Fetch ALL models dynamically from /v1/models (includes combos, custom, aliased) + // Per @alpgul feedback: /api/models/alias doesn't include combo definitions + useEffect(() => { + if (!isExpanded || modelsLoaded) return; + let cancelled = false; + fetch("/v1/models") + .then((res) => res.json()) + .then((data) => { + if (cancelled) return; + const modelList = (data.data || []) + .filter((m: any) => m.id) // Only models with valid IDs + .map((m: any) => ({ + value: m.id, + label: m.id, + })); + setAllModels(modelList); + setModelsLoaded(true); + }) + .catch(() => { + if (!cancelled) setModelsLoaded(true); + }); + return () => { + cancelled = true; + }; + }, [isExpanded, modelsLoaded]); + + // Filter models by search + const availableModels = searchFilter + ? allModels.filter((m) => m.label.toLowerCase().includes(searchFilter.toLowerCase())) + : allModels; + + // Persist selection + useEffect(() => { + if (selectedModels.size > 0) { + localStorage.setItem( + "omniroute-copilot-selected-models", + JSON.stringify([...selectedModels]) + ); + } + }, [selectedModels]); + + const toggleModel = (modelValue: string) => { + setSelectedModels((prev) => { + const next = new Set(prev); + if (next.has(modelValue)) { + next.delete(modelValue); + } else { + next.add(modelValue); + } + return next; + }); + }; + + const selectAll = () => { + setSelectedModels(new Set(allModels.map((m) => m.value))); + }; + + const deselectAll = () => { + setSelectedModels(new Set()); + }; + + const getBaseUrlForConfig = () => { + // Use window.location.origin directly — works correctly in Docker/reverse-proxy + // Per @alpgul feedback: don't use baseUrl prop (has port duplication issues) + const origin = + typeof window !== "undefined" ? window.location.origin : "http://localhost:20128"; + return `${origin}/v1/chat/completions`; + }; + + // Generate the Copilot chatLanguageModels.json config + const generateConfig = () => { + const models = [...selectedModels].map((modelId) => ({ + id: modelId, + name: modelId, + url: `${getBaseUrlForConfig()}#models.ai.azure.com`, + toolCalling, + vision, + maxInputTokens, + maxOutputTokens, + })); + + const config = { + name: "OmniRoute", + vendor: "azure", + apiKey: `\${input:chat.lm.secret.omniroute}`, + models, + }; + + return JSON.stringify(config, null, 2); + }; + + const handleCopy = async (text: string, field: string) => { + await navigator.clipboard.writeText(text); + setCopiedField(field); + setTimeout(() => setCopiedField(null), 2000); + }; + + const handleApiKeyChange = (value: string) => { + setSelectedApiKey(value); + if (value) localStorage.setItem("omniroute-cli-key-copilot", value); + }; + + return ( + + {/* Header */} +
+
+
+ {tool.name} { + (e.currentTarget as HTMLElement).style.display = "none"; + }} + /> +
+
+
+

{tool.name}

+ + + {t("guide")} + +
+

{tool.description}

+
+
+ + expand_more + +
+ + {/* Expanded content */} + {isExpanded && ( +
+
+ {/* Info box */} +
+ info +
+

GitHub Copilot Config Generator

+

+ Generates the{" "} + + chatLanguageModels.json + {" "} + block for VS Code GitHub Copilot using the Azure vendor pattern. Select the models + you want, then copy the JSON into your config file. +

+
+
+ + {/* Version compatibility warning */} +
+ warning +

+ This configuration uses the Azure vendor workaround for custom model lists. Tested + with VS Code ≥ 1.109 and{" "} + GitHub Copilot Chat ≥ v0.37. Future extension updates may change + this behavior. +

+
+ + {/* Step 2: API Key (if cloud enabled) */} + {cloudEnabled && apiKeys?.length > 0 && ( +
+
+
+ 1 +
+ API Key +
+ +
+ )} + + {/* Step 3: Model Selection */} +
+
+
+
+ {cloudEnabled && apiKeys?.length > 0 ? "2" : "1"} +
+ + Select Models ({selectedModels.size}/{availableModels.length}) + +
+
+ + +
+
+ + {/* Search filter */} +
+ setSearchFilter(e.target.value)} + placeholder="Filter models..." + className="w-full px-3 py-1.5 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" + /> +
+ + {!modelsLoaded && allModels.length === 0 ? ( +
+ + progress_activity + + Loading models... +
+ ) : availableModels.length === 0 && allModels.length === 0 ? ( +
+ warning +

+ {t("noActiveProviders")} +

+
+ ) : ( +
+ {availableModels.map((model) => ( + + ))} +
+ )} +
+ + {/* Step 4: Advanced options (collapsible) */} +
+ + + chevron_right + + Advanced Options + +
+
+ + setMaxInputTokens(Number(e.target.value) || 128000)} + className="w-full px-3 py-1.5 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" + /> +
+
+ + setMaxOutputTokens(Number(e.target.value) || 16000)} + className="w-full px-3 py-1.5 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" + /> +
+ + +
+
+ + {/* Step 5: Generated config */} + {selectedModels.size > 0 && ( +
+
+
+
+ {cloudEnabled && apiKeys?.length > 0 ? "3" : "2"} +
+ + Copy Config ({selectedModels.size} model{selectedModels.size !== 1 ? "s" : ""} + ) + +
+ +
+
+                  
+                    {generateConfig()}
+                  
+                
+ + {/* Usage instructions */} +
+

+ Paste into: + + ~/.config/Code/User/chatLanguageModels.json + +

+

+ Then reload VS Code and set the API key in the input prompt. +

+
+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx index 3cffbb3a2f..dfab5ec0cc 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx @@ -6,3 +6,4 @@ export { default as ClineToolCard } from "./ClineToolCard"; export { default as KiloToolCard } from "./KiloToolCard"; export { default as DefaultToolCard } from "./DefaultToolCard"; export { default as AntigravityToolCard } from "./AntigravityToolCard"; +export { default as CopilotToolCard } from "./CopilotToolCard"; diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index c9bb3a6299..4f37ecfbd2 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -164,6 +164,14 @@ export const CLI_TOOLS = { { id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium", alias: "gpt-oss-120b-medium" }, ], }, + copilot: { + id: "copilot", + name: "GitHub Copilot", + image: "/providers/copilot.png", + color: "#1F6FEB", + description: "GitHub Copilot Chat — VS Code Extension", + configType: "custom", + }, // HIDDEN: gemini-cli // "gemini-cli": { // id: "gemini-cli",