diff --git a/apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs b/apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs index 4028a688e9..3aa5518b59 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs @@ -334,6 +334,7 @@ fn workspace_provider(provider_id: &str) -> Option { "xai" => ProviderId::Xai, "v0" => ProviderId::V0, "helmcode" => ProviderId::Helmcode, + "gitkraken" => ProviderId::GitKraken, _ => return None, }) } @@ -389,7 +390,7 @@ fn litellm_workspace_change_allowed( mod tests { use codexbar::core::ProviderId; - use super::{litellm_workspace_change_allowed, workspace_provider}; + use super::{gateway_provider, litellm_workspace_change_allowed, workspace_provider}; #[test] fn maps_opencode_go_workspace_provider() { @@ -399,6 +400,18 @@ mod tests { ); } + #[test] + fn maps_gitkraken_organization_provider() { + assert_eq!(workspace_provider("gitkraken"), Some(ProviderId::GitKraken)); + } + + #[test] + fn gateway_provider_exposes_wayfinder_and_bifrost_only() { + assert_eq!(gateway_provider("wayfinder"), Some(ProviderId::Wayfinder)); + assert_eq!(gateway_provider("bifrost"), Some(ProviderId::Bifrost)); + assert_eq!(gateway_provider("codex"), None); + } + #[test] fn litellm_endpoint_change_requires_reentering_saved_key() { assert!( @@ -450,7 +463,18 @@ pub fn get_provider_workspace_id(provider_id: String) -> Result, } fn gateway_provider(provider_id: &str) -> Option { - (provider_id == "wayfinder").then_some(codexbar::core::ProviderId::Wayfinder) + match provider_id { + "wayfinder" => Some(codexbar::core::ProviderId::Wayfinder), + "bifrost" => Some(codexbar::core::ProviderId::Bifrost), + _ => None, + } +} + +#[tauri::command] +pub fn get_provider_gateway_url(provider_id: String) -> Result { + let id = gateway_provider(&provider_id) + .ok_or_else(|| format!("Provider '{provider_id}' does not expose a gateway URL"))?; + Ok(Settings::load().gateway_url(id).to_string()) } #[tauri::command] @@ -458,8 +482,17 @@ pub fn set_provider_gateway_url(provider_id: String, gateway_url: String) -> Res let id = gateway_provider(&provider_id) .ok_or_else(|| format!("Provider '{provider_id}' does not expose a gateway URL"))?; let gateway_url = gateway_url.trim(); - codexbar::providers::wayfinder::parse_gateway_url(gateway_url) - .map_err(|error| error.to_string())?; + match id { + codexbar::core::ProviderId::Wayfinder => { + codexbar::providers::wayfinder::parse_gateway_url(gateway_url) + .map_err(|error| error.to_string())?; + } + codexbar::core::ProviderId::Bifrost => { + codexbar::providers::bifrost::validate_gateway_url(gateway_url) + .map_err(|error| error.to_string())?; + } + _ => unreachable!("gateway_provider only returns gateway providers"), + } let mut settings = Settings::load(); settings.set_gateway_url(id, gateway_url.to_string()); diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index 347160e42b..30f6132c92 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -303,6 +303,7 @@ fn main() { commands::get_provider_region_options, commands::set_provider_workspace_id, commands::set_provider_gateway_url, + commands::get_provider_gateway_url, commands::get_provider_workspace_id, commands::get_gemini_cli_signed_in, commands::get_vertexai_status, diff --git a/apps/desktop-tauri/src/components/providers/providerIcons.ts b/apps/desktop-tauri/src/components/providers/providerIcons.ts index aefe0035aa..ee24a2c1f1 100644 --- a/apps/desktop-tauri/src/components/providers/providerIcons.ts +++ b/apps/desktop-tauri/src/components/providers/providerIcons.ts @@ -185,7 +185,10 @@ export const PROVIDER_ICON_REGISTRY: Record = { gemini: { id: "gemini", brandColor: "#ab87ea", fallbackLetter: "✦", svgPath: RAW.gemini }, grok: { id: "grok", brandColor: "#111827", fallbackLetter: "G", svgPath: RAW.grok }, groq: { id: "groq", brandColor: "#f55036", fallbackLetter: "G", svgPath: RAW.groq }, + bifrost: { id: "bifrost", brandColor: "#5b7cfa", fallbackLetter: "B" }, + gitkraken: { id: "gitkraken", brandColor: "#179287", fallbackLetter: "G" }, huggingface: { id: "huggingface", brandColor: "#ffd21e", fallbackLetter: "H", svgPath: RAW.huggingface }, + hyper: { id: "hyper", brandColor: "#7c3aed", fallbackLetter: "H" }, helmcode: { id: "helmcode", brandColor: "#4f46e5", fallbackLetter: "H" }, v0: { id: "v0", brandColor: "#111827", fallbackLetter: "V" }, typesafe: { id: "typesafe", brandColor: "#2563eb", fallbackLetter: "T" }, diff --git a/apps/desktop-tauri/src/lib/tauri.ts b/apps/desktop-tauri/src/lib/tauri.ts index 6ccd63c0de..57f6ce274d 100644 --- a/apps/desktop-tauri/src/lib/tauri.ts +++ b/apps/desktop-tauri/src/lib/tauri.ts @@ -450,6 +450,10 @@ export function setProviderGatewayUrl( return invoke("set_provider_gateway_url", { providerId, gatewayUrl }); } +export function getProviderGatewayUrl(providerId: string): Promise { + return invoke("get_provider_gateway_url", { providerId }); +} + export function getProviderAzureApiVersion(providerId: string): Promise { return invoke("get_provider_azure_api_version", { providerId }); } diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx index 0dadfe492d..ae3e6766a5 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useReducer } from "react"; +import { useCallback, useEffect, useReducer, useState } from "react"; import type { SettingsSnapshot, SettingsUpdate } from "../../../types/bridge"; import { useLocale } from "../../../hooks/useLocale"; import { providerAllowsPace } from "../../../lib/providerPace"; @@ -6,6 +6,7 @@ import { getCredentialStorageStatus, getProviderCookieSourceOptions, getProviderDetail, + getProviderGatewayUrl, getProviderRegionOptions, getTokenAccountProviders, openProviderDashboard, @@ -83,6 +84,8 @@ export function ProviderDetailPane({ onSettingsChange, }: Props) { const { t, language } = useLocale(); + const [gatewayLoadedProviderId, setGatewayLoadedProviderId] = + useState(null); const [state, dispatch] = useReducer( providerDetailPaneReducer, { wayfinderGatewayUrl, providerId }, @@ -142,11 +145,34 @@ export function ProviderDetailPane({ } }, []); + const gatewayProviderId = providerId === "wayfinder" || providerId === "bifrost" + ? providerId + : null; + + useEffect(() => { + setGatewayLoadedProviderId(null); + if (!gatewayProviderId) return; + let cancelled = false; + void getProviderGatewayUrl(gatewayProviderId).then((url) => { + if (!cancelled) { + dispatch({ type: "SET_GATEWAY_DRAFT", draft: url }); + setGatewayLoadedProviderId(gatewayProviderId); + } + }).catch((e) => { + if (!cancelled) { + dispatch({ type: "SAVE_GATEWAY_ERROR", error: String(e) }); + setGatewayLoadedProviderId(gatewayProviderId); + } + }); + return () => { cancelled = true; }; + }, [gatewayProviderId]); + const saveGateway = async () => { dispatch({ type: "SAVE_GATEWAY_START" }); try { - await setProviderGatewayUrl("wayfinder", gatewayDraft); - await load("wayfinder"); + if (!gatewayProviderId) return; + await setProviderGatewayUrl(gatewayProviderId, gatewayDraft); + await load(gatewayProviderId); } catch (e) { dispatch({ type: "SAVE_GATEWAY_ERROR", error: String(e) }); } finally { @@ -319,7 +345,8 @@ export function ProviderDetailPane({ t={t} onChanged={reload} /> - {detail.id === "wayfinder" && ( + {(detail.id === "wayfinder" || detail.id === "bifrost") && + gatewayLoadedProviderId === detail.id && ( void saveGateway()} t={t} + bifrost={detail.id === "bifrost"} /> )} ; case "openrouter": return ; diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/WayfinderGatewaySection.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/WayfinderGatewaySection.tsx index 1f0f28f0e6..77b4067771 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/WayfinderGatewaySection.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/WayfinderGatewaySection.tsx @@ -8,6 +8,7 @@ interface Props { onDraftChange: (draft: string) => void; onSave: () => void; t: (key: LocaleKey) => string; + bifrost?: boolean; } export function WayfinderGatewaySection({ @@ -18,12 +19,13 @@ export function WayfinderGatewaySection({ onDraftChange, onSave, t, + bifrost = false, }: Props) { return (
-

{t("WayfinderGatewayTitle")}

+

{bifrost ? "Bifrost gateway" : t("WayfinderGatewayTitle")}

-

{t("WayfinderGatewayHelp")}

+

+ {bifrost ? "Base URL of your Bifrost gateway." : t("WayfinderGatewayHelp")} +

{error &&

{error}

}