diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index d3d7bea7d..3259025bc 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -40,6 +40,7 @@ import ThinkingModeSelector, { thinkingModes } from './ThinkingModeSelector.jsx' import Fuse from 'fuse.js'; import CommandMenu from './CommandMenu'; import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants'; +import { isProviderEnabled, getDefaultProvider } from '../utils/providers'; import { safeJsonParse } from '../lib/utils.js'; @@ -1936,7 +1937,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, late const [claudeStatus, setClaudeStatus] = useState(null); const [thinkingMode, setThinkingMode] = useState('none'); const [provider, setProvider] = useState(() => { - return localStorage.getItem('selected-provider') || 'claude'; + const saved = localStorage.getItem('selected-provider'); + // Use saved value only if provider is still enabled + if (saved && isProviderEnabled(saved)) { + return saved; + } + // Fallback to default and sync localStorage + const fallback = getDefaultProvider(); + localStorage.setItem('selected-provider', fallback); + return fallback; }); const [cursorModel, setCursorModel] = useState(() => { return localStorage.getItem('cursor-model') || CURSOR_MODELS.DEFAULT; @@ -4960,100 +4969,106 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, late
{/* Claude Button */} - - + {provider === 'claude' && ( +
+
+ + + +
+
+ )} + + )} + {/* Cursor Button */} - + {provider === 'cursor' && ( +
+
+ + + +
+
+ )} + + )} {/* Codex Button */} - + {provider === 'codex' && ( +
+
+ + + +
+
+ )} + + )}
{/* Model Selection - Always reserve space to prevent jumping */} diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx index 0d828a8e8..bcdbc6d05 100644 --- a/src/components/Settings.jsx +++ b/src/components/Settings.jsx @@ -13,6 +13,7 @@ import GitSettings from './GitSettings'; import TasksSettings from './TasksSettings'; import LoginModal from './LoginModal'; import { authenticatedFetch } from '../utils/api'; +import { isProviderEnabled, getDefaultProvider } from '../utils/providers'; // New settings components import AgentListItem from './settings/AgentListItem'; @@ -58,7 +59,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) { const [mcpToolsLoading, setMcpToolsLoading] = useState({}); const [activeTab, setActiveTab] = useState(initialTab); const [jsonValidationError, setJsonValidationError] = useState(''); - const [selectedAgent, setSelectedAgent] = useState('claude'); // 'claude', 'cursor', or 'codex' + const [selectedAgent, setSelectedAgent] = useState(() => getDefaultProvider()); // Only enabled providers const [selectedCategory, setSelectedCategory] = useState('account'); // 'account', 'permissions', or 'mcp' // Code Editor settings @@ -1260,51 +1261,63 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) { {/* Mobile: Horizontal Agent Tabs */}
- setSelectedAgent('claude')} - isMobile={true} - /> - setSelectedAgent('cursor')} - isMobile={true} - /> - setSelectedAgent('codex')} - isMobile={true} - /> + {isProviderEnabled('claude') && ( + setSelectedAgent('claude')} + isMobile={true} + /> + )} + {isProviderEnabled('cursor') && ( + setSelectedAgent('cursor')} + isMobile={true} + /> + )} + {isProviderEnabled('codex') && ( + setSelectedAgent('codex')} + isMobile={true} + /> + )}
{/* Desktop: Sidebar - Agent List */}
- setSelectedAgent('claude')} - /> - setSelectedAgent('cursor')} - /> - setSelectedAgent('codex')} - /> + {isProviderEnabled('claude') && ( + setSelectedAgent('claude')} + /> + )} + {isProviderEnabled('cursor') && ( + setSelectedAgent('cursor')} + /> + )} + {isProviderEnabled('codex') && ( + setSelectedAgent('codex')} + /> + )}
diff --git a/src/utils/providers.js b/src/utils/providers.js new file mode 100644 index 000000000..4d9271856 --- /dev/null +++ b/src/utils/providers.js @@ -0,0 +1,92 @@ +/** + * Provider configuration utility + * + * Controls which AI providers are available in the UI. + * Set VITE_ENABLED_PROVIDERS in .env to customize. + * + * @module utils/providers + * @example + * // Environment variable examples: + * // VITE_ENABLED_PROVIDERS=claude - Only Claude Code + * // VITE_ENABLED_PROVIDERS=claude,cursor - Claude and Cursor + * // VITE_ENABLED_PROVIDERS=claude,cursor,codex - All providers (default) + */ + +/** + * List of all supported provider identifiers. + * @constant {string[]} + */ +const ALL_PROVIDERS = ['claude', 'cursor', 'codex']; + +/** + * Retrieves the list of enabled providers from environment variable. + * + * Parses the VITE_ENABLED_PROVIDERS environment variable and returns + * an array of valid provider names. Invalid provider names are filtered out. + * + * @function getEnabledProviders + * @returns {string[]} Array of enabled provider names (lowercase) + * @example + * // With VITE_ENABLED_PROVIDERS=claude,cursor + * getEnabledProviders(); // ['claude', 'cursor'] + * + * @example + * // Without VITE_ENABLED_PROVIDERS set + * getEnabledProviders(); // ['claude', 'cursor', 'codex'] + */ +export function getEnabledProviders() { + const envProviders = import.meta.env.VITE_ENABLED_PROVIDERS; + + if (!envProviders) { + // Default: all providers enabled + return ALL_PROVIDERS; + } + + // Parse comma-separated list and filter valid providers + const providers = envProviders + .split(',') + .map(p => p.trim().toLowerCase()) + .filter(p => ALL_PROVIDERS.includes(p)); + + // Fallback to claude if no valid providers + return providers.length > 0 ? providers : ['claude']; +} + +/** + * Checks if a specific provider is enabled. + * + * Safely handles null, undefined, and non-string inputs by returning false. + * + * @function isProviderEnabled + * @param {*} provider - Provider name to check (expected: 'claude', 'cursor', 'codex') + * @returns {boolean} True if provider is enabled, false otherwise + * @example + * // With VITE_ENABLED_PROVIDERS=claude + * isProviderEnabled('claude'); // true + * isProviderEnabled('cursor'); // false + * isProviderEnabled(null); // false + * isProviderEnabled(undefined); // false + */ +export function isProviderEnabled(provider) { + // Safely handle null, undefined, or non-string inputs + if (typeof provider !== 'string' || !provider.trim()) { + return false; + } + return getEnabledProviders().includes(provider.toLowerCase()); +} + +/** + * Gets the default provider (first enabled provider). + * + * Returns the first provider from the enabled list, falling back to 'claude' + * if somehow the list is empty (which shouldn't happen with current logic). + * + * @function getDefaultProvider + * @returns {string} The default provider name + * @example + * // With VITE_ENABLED_PROVIDERS=cursor,claude + * getDefaultProvider(); // 'cursor' + */ +export function getDefaultProvider() { + return getEnabledProviders()[0] || 'claude'; +}