diff --git a/src/ccs.ts b/src/ccs.ts index bc48ea45..d5c3cc68 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -11,6 +11,7 @@ import { displayWebSearchStatus, getWebSearchHookEnv, } from './utils/websearch-manager'; +import { getGlobalEnvConfig } from './config/unified-config-loader'; // Import extracted command handlers import { handleVersionCommand } from './commands/version-command'; @@ -494,7 +495,11 @@ async function main(): Promise { // Use --settings flag (backward compatible) const expandedSettingsPath = getSettingsPath(profileInfo.name); const webSearchEnv = getWebSearchHookEnv(); + // Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles + const globalEnvConfig = getGlobalEnvConfig(); + const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; const envVars: NodeJS.ProcessEnv = { + ...globalEnv, ...webSearchEnv, CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider }; diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 7f572dba..df134a36 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -14,7 +14,7 @@ import { getCcsDir } from '../utils/config-manager'; import { warn } from '../utils/ui'; import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types'; import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader'; -import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; +import { loadOrCreateUnifiedConfig, getGlobalEnvConfig } from '../config/unified-config-loader'; /** Settings file structure for user overrides */ interface ProviderSettings { @@ -380,6 +380,18 @@ export function getClaudeEnvVars( }; } +/** + * Get global env vars to inject into all third-party profiles. + * Returns empty object if disabled. + */ +function getGlobalEnvVars(): Record { + const globalEnvConfig = getGlobalEnvConfig(); + if (!globalEnvConfig.enabled) { + return {}; + } + return globalEnvConfig.env; +} + /** * Get effective environment variables for provider * @@ -388,7 +400,7 @@ export function getClaudeEnvVars( * 2. User settings file (~/.ccs/{provider}.settings.json) if exists * 3. Bundled defaults from PROVIDER_CONFIGS * - * This allows users to customize model mappings without code changes. + * All results are merged with global_env vars (telemetry/reporting disables). * User takes full responsibility for custom settings. */ export function getEffectiveEnvVars( @@ -396,6 +408,9 @@ export function getEffectiveEnvVars( port: number = CLIPROXY_DEFAULT_PORT, customSettingsPath?: string ): NodeJS.ProcessEnv { + // Get global env vars (DISABLE_TELEMETRY, etc.) + const globalEnv = getGlobalEnvVars(); + // Priority 1: Custom settings path (for user-defined variants) if (customSettingsPath) { const expandedPath = customSettingsPath.replace(/^~/, require('os').homedir()); @@ -405,8 +420,8 @@ export function getEffectiveEnvVars( const settings: ProviderSettings = JSON.parse(content); if (settings.env && typeof settings.env === 'object') { - // Custom variant settings found - use them - return settings.env; + // Custom variant settings found - merge with global env + return { ...globalEnv, ...settings.env }; } } catch { // Invalid JSON - fall through to provider defaults @@ -427,9 +442,8 @@ export function getEffectiveEnvVars( const settings: ProviderSettings = JSON.parse(content); if (settings.env && typeof settings.env === 'object') { - // User override found - use their settings - // Note: User is responsible for correctness - return settings.env; + // User override found - merge with global env + return { ...globalEnv, ...settings.env }; } } catch { // Invalid JSON or structure - fall through to defaults @@ -437,8 +451,8 @@ export function getEffectiveEnvVars( } } - // No override or invalid - use bundled defaults - return getClaudeEnvVars(provider, port); + // No override or invalid - use bundled defaults merged with global env + return { ...globalEnv, ...getClaudeEnvVars(provider, port) }; } /** diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index de6efa48..80e0ad64 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -15,6 +15,8 @@ import { createEmptyUnifiedConfig, UNIFIED_CONFIG_VERSION, DEFAULT_COPILOT_CONFIG, + DEFAULT_GLOBAL_ENV, + GlobalEnvConfig, } from './unified-config-types'; import { isUnifiedConfigEnabled } from './feature-flags'; @@ -170,6 +172,11 @@ function mergeWithDefaults(partial: Partial): UnifiedConfig { wait_on_limit: partial.copilot?.wait_on_limit ?? DEFAULT_COPILOT_CONFIG.wait_on_limit, model: partial.copilot?.model ?? DEFAULT_COPILOT_CONFIG.model, }, + // Global env - injected into all non-Claude subscription profiles + global_env: { + enabled: partial.global_env?.enabled ?? true, + env: partial.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV }, + }, }; } @@ -336,6 +343,28 @@ function generateYamlWithComments(config: UnifiedConfig): string { lines.push(''); } + // Global env section + if (config.global_env) { + lines.push('# ----------------------------------------------------------------------------'); + lines.push( + '# Global Environment Variables: Injected into all non-Claude subscription profiles' + ); + lines.push('# These env vars disable telemetry/reporting for third-party providers.'); + lines.push('# Configure via Dashboard (`ccs config`) > Global Env tab.'); + lines.push('#'); + lines.push('# Default variables:'); + lines.push('# DISABLE_BUG_COMMAND: Disables /bug command (not supported by proxy)'); + lines.push('# DISABLE_ERROR_REPORTING: Disables error reporting to Anthropic'); + lines.push('# DISABLE_TELEMETRY: Disables usage telemetry'); + lines.push('# ----------------------------------------------------------------------------'); + lines.push( + yaml + .dump({ global_env: config.global_env }, { indent: 2, lineWidth: -1, quotingType: '"' }) + .trim() + ); + lines.push(''); + } + return lines.join('\n'); } @@ -462,3 +491,15 @@ export function getWebSearchConfig(): { gemini: config.websearch?.gemini, }; } + +/** + * Get global_env configuration. + * Returns defaults if not configured. + */ +export function getGlobalEnvConfig(): GlobalEnvConfig { + const config = loadOrCreateUnifiedConfig(); + return { + enabled: config.global_env?.enabled ?? true, + env: config.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV }, + }; +} diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index a692173c..0379c9ea 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -185,6 +185,28 @@ export interface CopilotConfig { haiku_model?: string; } +/** + * Global environment variables configuration. + * These env vars are injected into ALL non-Claude subscription profiles. + * Useful for disabling telemetry, bug commands, error reporting, etc. + */ +export interface GlobalEnvConfig { + /** Enable global env injection (default: true) */ + enabled: boolean; + /** Environment variables to inject */ + env: Record; +} + +/** + * Default global env vars for third-party profiles. + * These disable Claude Code telemetry/reporting since we're using proxy. + */ +export const DEFAULT_GLOBAL_ENV: Record = { + DISABLE_BUG_COMMAND: '1', + DISABLE_ERROR_REPORTING: '1', + DISABLE_TELEMETRY: '1', +}; + /** * WebSearch configuration. * Uses CLI tools (Gemini CLI, Grok CLI, OpenCode) for third-party profiles. @@ -234,6 +256,8 @@ export interface UnifiedConfig { preferences: PreferencesConfig; /** WebSearch configuration */ websearch?: WebSearchConfig; + /** Global environment variables for all non-Claude subscription profiles */ + global_env?: GlobalEnvConfig; /** Copilot API configuration (GitHub Copilot proxy) */ copilot?: CopilotConfig; } @@ -307,6 +331,10 @@ export function createEmptyUnifiedConfig(): UnifiedConfig { }, }, }, + global_env: { + enabled: true, + env: { ...DEFAULT_GLOBAL_ENV }, + }, copilot: { ...DEFAULT_COPILOT_CONFIG }, }; } diff --git a/src/copilot/copilot-executor.ts b/src/copilot/copilot-executor.ts index 012fb385..794cf8ff 100644 --- a/src/copilot/copilot-executor.ts +++ b/src/copilot/copilot-executor.ts @@ -7,6 +7,7 @@ import { spawn } from 'child_process'; import { CopilotConfig } from '../config/unified-config-types'; +import { getGlobalEnvConfig } from '../config/unified-config-loader'; import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth'; import { isDaemonRunning, startDaemon } from './copilot-daemon'; import { ensureCopilotApi } from './copilot-package-manager'; @@ -127,9 +128,14 @@ export async function executeCopilotProfile( // Generate environment for Claude const copilotEnv = generateCopilotEnv(config); - // Merge with current environment + // Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles + const globalEnvConfig = getGlobalEnvConfig(); + const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; + + // Merge with current environment (global env first, copilot overrides) const env = { ...process.env, + ...globalEnv, ...copilotEnv, }; diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 7ccd973d..9e1cd207 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -1789,7 +1789,7 @@ import { getInstalledVersion as getCopilotInstalledVersion, } from '../copilot'; import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types'; -import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; +import { loadOrCreateUnifiedConfig, getGlobalEnvConfig } from '../config/unified-config-loader'; /** * GET /api/copilot/status - Get Copilot status (auth + daemon + install info) @@ -2081,3 +2081,50 @@ apiRoutes.put('/copilot/settings/raw', (req: Request, res: Response): void => { res.status(500).json({ error: (error as Error).message }); } }); + +// ==================== Global Environment Variables ==================== + +/** + * GET /api/global-env - Get global environment variables configuration + * Returns the global_env section from config.yaml + */ +apiRoutes.get('/global-env', (_req: Request, res: Response): void => { + try { + const config = getGlobalEnvConfig(); + res.json(config); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + +/** + * PUT /api/global-env - Update global environment variables configuration + * Updates the global_env section in config.yaml + */ +apiRoutes.put('/global-env', (req: Request, res: Response): void => { + try { + const { enabled, env } = req.body; + const config = loadOrCreateUnifiedConfig(); + + // Validate env is an object with string values + if (env !== undefined && typeof env === 'object' && env !== null) { + for (const [key, value] of Object.entries(env)) { + if (typeof value !== 'string') { + res.status(400).json({ error: `Invalid value for ${key}: must be a string` }); + return; + } + } + } + + // Update global_env section + config.global_env = { + enabled: enabled ?? config.global_env?.enabled ?? true, + env: env ?? config.global_env?.env ?? {}, + }; + + saveUnifiedConfig(config); + res.json({ success: true, config: config.global_env }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); diff --git a/ui/src/components/cliproxy/provider-editor.tsx b/ui/src/components/cliproxy/provider-editor.tsx index 98b2f5bf..0fd9632e 100644 --- a/ui/src/components/cliproxy/provider-editor.tsx +++ b/ui/src/components/cliproxy/provider-editor.tsx @@ -57,6 +57,7 @@ import { useDeletePreset, } from '@/hooks/use-cliproxy'; import { cn } from '@/lib/utils'; +import { GlobalEnvIndicator } from '@/components/global-env-indicator'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; // Lazy load CodeEditor @@ -543,7 +544,7 @@ export function ProviderEditor({ Invalid JSON syntax )} -
+
+ {/* Global Env Indicator */} +
+
+ +
+
); diff --git a/ui/src/components/copilot/copilot-config-form.tsx b/ui/src/components/copilot/copilot-config-form.tsx index 80cce9db..2311c6b2 100644 --- a/ui/src/components/copilot/copilot-config-form.tsx +++ b/ui/src/components/copilot/copilot-config-form.tsx @@ -31,6 +31,7 @@ import { useCopilot, type CopilotModel, type CopilotPlanTier } from '@/hooks/use import { Loader2, Save, Code2, X, Info, RefreshCw, Sparkles, Zap, Check } from 'lucide-react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/confirm-dialog'; +import { GlobalEnvIndicator } from '@/components/global-env-indicator'; // Lazy load CodeEditor const CodeEditor = lazy(() => @@ -726,7 +727,7 @@ export function CopilotConfigForm() { Invalid JSON syntax )} -
+
+ {/* Global Env Indicator */} +
+
+ +
+
); diff --git a/ui/src/components/global-env-indicator.tsx b/ui/src/components/global-env-indicator.tsx new file mode 100644 index 00000000..b6cc0cf3 --- /dev/null +++ b/ui/src/components/global-env-indicator.tsx @@ -0,0 +1,132 @@ +/** + * Global Environment Variables Indicator + * + * Shows which env vars from global_env will be injected at runtime. + * Displayed below the Raw Configuration (JSON) section in profile editors. + */ + +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Settings2, ChevronDown, ChevronUp, ExternalLink, Info } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface GlobalEnvConfig { + enabled: boolean; + env: Record; +} + +interface GlobalEnvIndicatorProps { + /** Current profile's env vars (to show which are overridden) */ + profileEnv?: Record; +} + +export function GlobalEnvIndicator({ profileEnv = {} }: GlobalEnvIndicatorProps) { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + fetchConfig(); + }, []); + + const fetchConfig = async () => { + try { + setLoading(true); + const res = await fetch('/api/global-env'); + if (!res.ok) throw new Error('Failed to load'); + const data = await res.json(); + setConfig(data); + } catch { + setConfig(null); + } finally { + setLoading(false); + } + }; + + // Don't render if loading or disabled or no vars + if (loading) return null; + if (!config?.enabled) return null; + + const envVars = config.env || {}; + const envKeys = Object.keys(envVars); + if (envKeys.length === 0) return null; + + // Check which keys are already in profile (won't be overridden) + const injectedKeys = envKeys.filter((key) => !(key in profileEnv)); + const overriddenKeys = envKeys.filter((key) => key in profileEnv); + + return ( +
+ {/* Header - clickable to expand */} + + + {/* Expanded content */} + {expanded && ( +
+ {/* Injected vars */} + {injectedKeys.length > 0 && ( +
+ {injectedKeys.map((key) => ( +
+ + + + {key}={envVars[key]} + +
+ ))} +
+ )} + + {/* Overridden vars (profile takes precedence) */} + {overriddenKeys.length > 0 && ( +
+

Skipped (profile already defines):

+ {overriddenKeys.map((key) => ( +
+ ~ + {key} +
+ ))} +
+ )} + + {/* Link to settings */} +
+ +
+
+ )} +
+ ); +} diff --git a/ui/src/components/profile-editor.tsx b/ui/src/components/profile-editor.tsx index 5a763587..b582c00d 100644 --- a/ui/src/components/profile-editor.tsx +++ b/ui/src/components/profile-editor.tsx @@ -16,6 +16,7 @@ import { Save, Loader2, Code2, Trash2, RefreshCw, Plus, X, Info } from 'lucide-r import { toast } from 'sonner'; import { CopyButton } from '@/components/ui/copy-button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { GlobalEnvIndicator } from '@/components/global-env-indicator'; // Lazy load CodeEditor to reduce initial bundle size const CodeEditor = lazy(() => @@ -412,7 +413,7 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) { Invalid JSON syntax )} -
+
+ {/* Global Env Indicator */} +
+
+ +
+
); diff --git a/ui/src/pages/settings.tsx b/ui/src/pages/settings.tsx index d14175db..00f39dee 100644 --- a/ui/src/pages/settings.tsx +++ b/ui/src/pages/settings.tsx @@ -1,15 +1,17 @@ /** - * Settings Page - WebSearch Configuration - * Supports Gemini CLI and Grok CLI providers + * Settings Page - WebSearch & Global Env Configuration + * Supports Gemini CLI and Grok CLI providers + Global Environment Variables */ import { useState, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Globe, RefreshCw, @@ -23,6 +25,9 @@ import { ExternalLink, ChevronDown, ChevronUp, + Settings2, + Plus, + Trash2, } from 'lucide-react'; import { CodeEditor } from '@/components/code-editor'; @@ -59,7 +64,15 @@ interface WebSearchStatus { }; } +interface GlobalEnvConfig { + enabled: boolean; + env: Record; +} + export function SettingsPage() { + const [searchParams] = useSearchParams(); + const initialTab = searchParams.get('tab') === 'globalenv' ? 'globalenv' : 'websearch'; + const [activeTab, setActiveTab] = useState<'websearch' | 'globalenv'>(initialTab); const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -78,12 +91,22 @@ export function SettingsPage() { const [showGeminiHint, setShowGeminiHint] = useState(false); const [showOpencodeHint, setShowOpencodeHint] = useState(false); const [showGrokHint, setShowGrokHint] = useState(false); + // Global Env state + const [globalEnvConfig, setGlobalEnvConfig] = useState(null); + const [globalEnvLoading, setGlobalEnvLoading] = useState(true); + const [globalEnvSaving, setGlobalEnvSaving] = useState(false); + const [globalEnvError, setGlobalEnvError] = useState(null); + const [globalEnvSuccess, setGlobalEnvSuccess] = useState(false); + // New env var inputs + const [newEnvKey, setNewEnvKey] = useState(''); + const [newEnvValue, setNewEnvValue] = useState(''); // Load config and status on mount useEffect(() => { fetchConfig(); fetchStatus(); fetchRawConfig(); + fetchGlobalEnvConfig(); }, []); // Sync local model inputs when config changes @@ -141,6 +164,21 @@ export function SettingsPage() { } }; + const fetchGlobalEnvConfig = async () => { + try { + setGlobalEnvLoading(true); + setGlobalEnvError(null); + const res = await fetch('/api/global-env'); + if (!res.ok) throw new Error('Failed to load Global Env config'); + const data = await res.json(); + setGlobalEnvConfig(data); + } catch (err) { + setGlobalEnvError((err as Error).message); + } finally { + setGlobalEnvLoading(false); + } + }; + const copyToClipboard = async () => { if (!rawConfig) return; try { @@ -284,6 +322,71 @@ export function SettingsPage() { } }; + // Global Env functions + const saveGlobalEnvConfig = async (updates: Partial) => { + if (!globalEnvConfig) return; + + // Optimistic update + const optimisticConfig = { ...globalEnvConfig, ...updates }; + setGlobalEnvConfig(optimisticConfig); + + try { + setGlobalEnvSaving(true); + setGlobalEnvError(null); + + const res = await fetch('/api/global-env', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(optimisticConfig), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'Failed to save'); + } + + const data = await res.json(); + setGlobalEnvConfig(data.config); + setGlobalEnvSuccess(true); + setTimeout(() => setGlobalEnvSuccess(false), 1500); + // Silently refresh raw config + fetch('/api/config/raw') + .then((r) => (r.ok ? r.text() : null)) + .then((text) => text && setRawConfig(text)) + .catch(() => {}); + } catch (err) { + setGlobalEnvConfig(globalEnvConfig); + setGlobalEnvError((err as Error).message); + } finally { + setGlobalEnvSaving(false); + } + }; + + const toggleGlobalEnv = () => { + saveGlobalEnvConfig({ enabled: !globalEnvConfig?.enabled }); + }; + + const addEnvVar = () => { + if (!newEnvKey.trim() || !globalEnvConfig) return; + const newEnv = { ...globalEnvConfig.env, [newEnvKey.trim()]: newEnvValue }; + saveGlobalEnvConfig({ env: newEnv }); + setNewEnvKey(''); + setNewEnvValue(''); + }; + + const removeEnvVar = (key: string) => { + if (!globalEnvConfig) return; + const newEnv = { ...globalEnvConfig.env }; + delete newEnv[key]; + saveGlobalEnvConfig({ env: newEnv }); + }; + + const updateEnvValue = (key: string, value: string) => { + if (!globalEnvConfig) return; + const newEnv = { ...globalEnvConfig.env, [key]: value }; + saveGlobalEnvConfig({ env: newEnv }); + }; + if (loading) { return (
@@ -298,348 +401,79 @@ export function SettingsPage() { return (
- {/* Left Panel - WebSearch Controls */} + {/* Left Panel - Settings Controls */}
- {/* Header */} + {/* Header with Tabs */}
-
- -
-

WebSearch

-

- CLI-based web search for third-party profiles -

-
-
-
- - {/* Toast-style alerts - absolute positioned, no layout shift */} -
- {error && ( - - - {error} - - )} - {success && ( -
- - Saved -
- )} -
- - {/* Scrollable Content */} - -
- {/* Status Summary */} -
-
-

- {isGeminiEnabled ? 'WebSearch enabled' : 'WebSearch disabled'} -

- {statusLoading ? ( -

Checking status...

- ) : status?.readiness ? ( -

{status.readiness.message}

- ) : null} -
- -
- - {/* CLI Providers */} -
-

Providers

- - {/* Gemini CLI Provider */} -
-
-
- -
-
-

gemini

- - FREE - - {status?.geminiCli?.installed ? ( - - installed - - ) : ( - - not installed - - )} -
-

- Google Gemini CLI (1000 req/day free) -

-
-
- -
- {/* Model input when enabled */} - {isGeminiEnabled && ( -
-
- - setGeminiModelInput(e.target.value)} - onBlur={saveGeminiModel} - placeholder="gemini-2.5-flash" - className="h-8 text-sm font-mono" - disabled={saving} - /> -
-
- )} - {/* Installation hint when not installed - inside card */} - {!status?.geminiCli?.installed && !statusLoading && ( -
- - {showGeminiHint && ( -
-

- Install globally (FREE tier available): -

- - npm install -g @google/gemini-cli - - - - View documentation - -
- )} -
- )} -
- - {/* OpenCode CLI Provider */} -
-
-
- -
-
-

opencode

- - FREE - - {status?.opencodeCli?.installed ? ( - - installed - - ) : ( - - not installed - - )} -
-

- OpenCode (web search via Zen) -

-
-
- -
- {/* Model input when enabled */} - {isOpenCodeEnabled && ( -
-
- - setOpencodeModelInput(e.target.value)} - onBlur={saveOpencodeModel} - placeholder="opencode/grok-code" - className="h-8 text-sm font-mono" - disabled={saving} - /> -
-
- )} - {/* Installation hint when not installed - inside card */} - {!status?.opencodeCli?.installed && !statusLoading && ( -
- - {showOpencodeHint && ( -
-

- Install globally (FREE tier available): -

- - curl -fsSL https://opencode.ai/install | bash - - - - View documentation - -
- )} -
- )} -
- - {/* Grok CLI Provider */} -
-
-
- -
-
-

grok

- - GROK_API_KEY - - {status?.grokCli?.installed ? ( - - installed - - ) : ( - - not installed - - )} -
-

- xAI Grok CLI (web + X search) -

-
-
- -
- {/* Installation hint when not installed - inside card */} - {!status?.grokCli?.installed && !statusLoading && ( -
- - {showGrokHint && ( -
-

- Install globally (requires xAI API key): -

- - npm install -g @vibe-kit/grok-cli - - - - View documentation - -
- )} -
- )} -
-
-
-
- - {/* Footer */} -
- + + + + WebSearch + + + + Global Env + + +
+ + {/* Tab Content */} + {activeTab === 'websearch' ? ( + + ) : ( + + )}
@@ -719,3 +553,613 @@ export function SettingsPage() {
); } + +// WebSearch Tab Content Component +interface WebSearchContentProps { + config: WebSearchConfig | null; + status: WebSearchStatus | null; + statusLoading: boolean; + saving: boolean; + error: string | null; + success: boolean; + isGeminiEnabled: boolean; + isGrokEnabled: boolean; + isOpenCodeEnabled: boolean; + geminiModelInput: string; + opencodeModelInput: string; + showGeminiHint: boolean; + showOpencodeHint: boolean; + showGrokHint: boolean; + setGeminiModelInput: (v: string) => void; + setOpencodeModelInput: (v: string) => void; + setShowGeminiHint: (v: boolean) => void; + setShowOpencodeHint: (v: boolean) => void; + setShowGrokHint: (v: boolean) => void; + toggleGemini: () => void; + toggleGrok: () => void; + toggleOpenCode: () => void; + saveGeminiModel: () => void; + saveOpencodeModel: () => void; + fetchStatus: () => void; + fetchConfig: () => void; + fetchRawConfig: () => void; + loading: boolean; +} + +function WebSearchContent({ + status, + statusLoading, + saving, + error, + success, + isGeminiEnabled, + isGrokEnabled, + isOpenCodeEnabled, + geminiModelInput, + opencodeModelInput, + showGeminiHint, + showOpencodeHint, + showGrokHint, + setGeminiModelInput, + setOpencodeModelInput, + setShowGeminiHint, + setShowOpencodeHint, + setShowGrokHint, + toggleGemini, + toggleGrok, + toggleOpenCode, + saveGeminiModel, + saveOpencodeModel, + fetchStatus, + fetchConfig, + fetchRawConfig, + loading, +}: WebSearchContentProps) { + return ( + <> + {/* Toast-style alerts - absolute positioned, no layout shift */} +
+ {error && ( + + + {error} + + )} + {success && ( +
+ + Saved +
+ )} +
+ + {/* Scrollable Content */} + +
+ {/* Description */} +

+ CLI-based web search for third-party profiles (gemini, codex, agy, etc.) +

+ + {/* Status Summary */} +
+
+

+ {isGeminiEnabled ? 'WebSearch enabled' : 'WebSearch disabled'} +

+ {statusLoading ? ( +

Checking status...

+ ) : status?.readiness ? ( +

{status.readiness.message}

+ ) : null} +
+ +
+ + {/* CLI Providers */} +
+

Providers

+ + {/* Gemini CLI Provider */} +
+
+
+ +
+
+

gemini

+ + FREE + + {status?.geminiCli?.installed ? ( + + installed + + ) : ( + + not installed + + )} +
+

+ Google Gemini CLI (1000 req/day free) +

+
+
+ +
+ {/* Model input when enabled */} + {isGeminiEnabled && ( +
+
+ + setGeminiModelInput(e.target.value)} + onBlur={saveGeminiModel} + placeholder="gemini-2.5-flash" + className="h-8 text-sm font-mono" + disabled={saving} + /> +
+
+ )} + {/* Installation hint when not installed - inside card */} + {!status?.geminiCli?.installed && !statusLoading && ( +
+ + {showGeminiHint && ( +
+

+ Install globally (FREE tier available): +

+ + npm install -g @google/gemini-cli + + + + View documentation + +
+ )} +
+ )} +
+ + {/* OpenCode CLI Provider */} +
+
+
+ +
+
+

opencode

+ + FREE + + {status?.opencodeCli?.installed ? ( + + installed + + ) : ( + + not installed + + )} +
+

OpenCode (web search via Zen)

+
+
+ +
+ {/* Model input when enabled */} + {isOpenCodeEnabled && ( +
+
+ + setOpencodeModelInput(e.target.value)} + onBlur={saveOpencodeModel} + placeholder="opencode/grok-code" + className="h-8 text-sm font-mono" + disabled={saving} + /> +
+
+ )} + {/* Installation hint when not installed - inside card */} + {!status?.opencodeCli?.installed && !statusLoading && ( +
+ + {showOpencodeHint && ( +
+

+ Install globally (FREE tier available): +

+ + curl -fsSL https://opencode.ai/install | bash + + + + View documentation + +
+ )} +
+ )} +
+ + {/* Grok CLI Provider */} +
+
+
+ +
+
+

grok

+ + GROK_API_KEY + + {status?.grokCli?.installed ? ( + + installed + + ) : ( + + not installed + + )} +
+

xAI Grok CLI (web + X search)

+
+
+ +
+ {/* Installation hint when not installed - inside card */} + {!status?.grokCli?.installed && !statusLoading && ( +
+ + {showGrokHint && ( +
+

+ Install globally (requires xAI API key): +

+ + npm install -g @vibe-kit/grok-cli + + + + View documentation + +
+ )} +
+ )} +
+
+
+
+ + {/* Footer */} +
+ +
+ + ); +} + +// Global Env Tab Content Component +interface GlobalEnvContentProps { + config: GlobalEnvConfig | null; + loading: boolean; + saving: boolean; + error: string | null; + success: boolean; + newEnvKey: string; + newEnvValue: string; + setNewEnvKey: (v: string) => void; + setNewEnvValue: (v: string) => void; + toggleGlobalEnv: () => void; + addEnvVar: () => void; + removeEnvVar: (key: string) => void; + updateEnvValue: (key: string, value: string) => void; + fetchGlobalEnvConfig: () => void; + fetchRawConfig: () => void; +} + +function GlobalEnvContent({ + config, + loading, + saving, + error, + success, + newEnvKey, + newEnvValue, + setNewEnvKey, + setNewEnvValue, + toggleGlobalEnv, + addEnvVar, + removeEnvVar, + fetchGlobalEnvConfig, + fetchRawConfig, +}: GlobalEnvContentProps) { + if (loading) { + return ( +
+
+ + Loading... +
+
+ ); + } + + return ( + <> + {/* Toast-style alerts */} +
+ {error && ( + + + {error} + + )} + {success && ( +
+ + Saved +
+ )} +
+ + {/* Scrollable Content */} + +
+ {/* Description */} +

+ Environment variables injected into all non-Claude subscription profiles (gemini, codex, + agy, copilot, etc.) +

+ + {/* Enable/Disable Toggle */} +
+
+

+ {config?.enabled ? 'Global Env enabled' : 'Global Env disabled'} +

+

+ {config?.enabled + ? 'Env vars will be injected into third-party profiles' + : 'Env vars will not be injected'} +

+
+ +
+ + {/* Current Environment Variables */} +
+

Environment Variables

+ + {config?.env && Object.keys(config.env).length > 0 ? ( +
+ {Object.entries(config.env).map(([key, value]) => ( +
+ {key} + = + {value} + +
+ ))} +
+ ) : ( +
+

No environment variables configured

+
+ )} + + {/* Add New Variable */} +
+

Add New Variable

+
+ setNewEnvKey(e.target.value.toUpperCase())} + placeholder="KEY_NAME" + className="flex-1 font-mono text-sm h-9" + disabled={saving} + /> + = + setNewEnvValue(e.target.value)} + placeholder="value" + className="flex-1 font-mono text-sm h-9" + disabled={saving} + /> + +
+
+ + {/* Common Variables Quick Add */} +
+

Quick Add Common Variables

+
+ {[ + { key: 'DISABLE_BUG_COMMAND', value: '1' }, + { key: 'DISABLE_ERROR_REPORTING', value: '1' }, + { key: 'DISABLE_TELEMETRY', value: '1' }, + ].map( + ({ key, value }) => + !config?.env?.[key] && ( + + ) + )} + {config?.env && + ['DISABLE_BUG_COMMAND', 'DISABLE_ERROR_REPORTING', 'DISABLE_TELEMETRY'].every( + (k) => config.env[k] + ) && ( + + All common variables are configured + + )} +
+
+
+
+
+ + {/* Footer */} +
+ +
+ + ); +}