From c602314312cbc10907f299dd6e5889c72d50e881 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 28 Dec 2025 01:19:30 +0100 Subject: [PATCH] feat: Implement Provider Tabs in Settings View - Added a new `ProviderTabs` component to manage different AI providers (Claude and Cursor) within the settings view. - Created `ClaudeSettingsTab` and `CursorSettingsTab` components for provider-specific configurations. - Updated navigation to reflect the new provider structure, replacing the previous Claude-only setup. - Marked completion of the settings view provider tabs phase in the integration plan. --- .../ui/src/components/views/settings-view.tsx | 36 +- .../views/settings-view/config/navigation.ts | 4 +- .../settings-view/hooks/use-settings-view.ts | 1 + .../providers/claude-settings-tab.tsx | 35 ++ .../providers/cursor-settings-tab.tsx | 321 ++++++++++++++++++ .../views/settings-view/providers/index.ts | 3 + .../settings-view/providers/provider-tabs.tsx | 36 ++ plan/cursor-cli-integration/README.md | 2 +- .../phases/phase-7-settings.md | 12 +- 9 files changed, 410 insertions(+), 40 deletions(-) create mode 100644 apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx create mode 100644 apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx create mode 100644 apps/ui/src/components/views/settings-view/providers/index.ts create mode 100644 apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx index b888c9b6..a5d420a8 100644 --- a/apps/ui/src/components/views/settings-view.tsx +++ b/apps/ui/src/components/views/settings-view.tsx @@ -1,16 +1,13 @@ import { useState } from 'react'; import { useAppStore } from '@/store/app-store'; -import { useCliStatus, useSettingsView } from './settings-view/hooks'; +import { useSettingsView } from './settings-view/hooks'; import { NAV_ITEMS } from './settings-view/config/navigation'; import { SettingsHeader } from './settings-view/components/settings-header'; import { KeyboardMapDialog } from './settings-view/components/keyboard-map-dialog'; import { DeleteProjectDialog } from './settings-view/components/delete-project-dialog'; import { SettingsNavigation } from './settings-view/components/settings-navigation'; import { ApiKeysSection } from './settings-view/api-keys/api-keys-section'; -import { ClaudeUsageSection } from './settings-view/api-keys/claude-usage-section'; -import { ClaudeCliStatus } from './settings-view/cli-status/claude-cli-status'; -import { ClaudeMdSettings } from './settings-view/claude/claude-md-settings'; import { AIEnhancementSection } from './settings-view/ai-enhancement'; import { AppearanceSection } from './settings-view/appearance/appearance-section'; import { TerminalSection } from './settings-view/terminal/terminal-section'; @@ -18,6 +15,7 @@ import { AudioSection } from './settings-view/audio/audio-section'; import { KeyboardShortcutsSection } from './settings-view/keyboard-shortcuts/keyboard-shortcuts-section'; import { FeatureDefaultsSection } from './settings-view/feature-defaults/feature-defaults-section'; import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section'; +import { ProviderTabs } from './settings-view/providers'; import type { Project as SettingsProject, Theme } from './settings-view/shared/types'; import type { Project as ElectronProject } from '@/lib/electron'; @@ -45,19 +43,10 @@ export function SettingsView() { defaultAIProfileId, setDefaultAIProfileId, aiProfiles, - apiKeys, validationModel, setValidationModel, - autoLoadClaudeMd, - setAutoLoadClaudeMd, } = useAppStore(); - // Hide usage tracking when using API key (only show for Claude Code CLI users) - // Also hide on Windows for now (CLI usage command not supported) - const isWindows = - typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win'); - const showUsageTracking = !apiKeys.anthropic && !isWindows; - // Convert electron Project to settings-view Project type const convertProject = (project: ElectronProject | null): SettingsProject | null => { if (!project) return null; @@ -85,9 +74,6 @@ export function SettingsView() { } }; - // Use CLI status hook - const { claudeCliStatus, isCheckingClaudeCli, handleRefreshClaudeCli } = useCliStatus(); - // Use settings view navigation hook const { activeView, navigateTo } = useSettingsView(); @@ -97,21 +83,9 @@ export function SettingsView() { // Render the active section based on current view const renderActiveSection = () => { switch (activeView) { - case 'claude': - return ( -
- - - {showUsageTracking && } -
- ); + case 'providers': + case 'claude': // Backwards compatibility + return ; case 'ai-enhancement': return ; case 'appearance': diff --git a/apps/ui/src/components/views/settings-view/config/navigation.ts b/apps/ui/src/components/views/settings-view/config/navigation.ts index e32c2223..001006c0 100644 --- a/apps/ui/src/components/views/settings-view/config/navigation.ts +++ b/apps/ui/src/components/views/settings-view/config/navigation.ts @@ -1,7 +1,7 @@ import type { LucideIcon } from 'lucide-react'; import { Key, - Terminal, + Bot, SquareTerminal, Palette, Settings2, @@ -21,7 +21,7 @@ export interface NavigationItem { // Navigation items for the settings side panel export const NAV_ITEMS: NavigationItem[] = [ { id: 'api-keys', label: 'API Keys', icon: Key }, - { id: 'claude', label: 'Claude', icon: Terminal }, + { id: 'providers', label: 'AI Providers', icon: Bot }, { id: 'ai-enhancement', label: 'AI Enhancement', icon: Sparkles }, { id: 'appearance', label: 'Appearance', icon: Palette }, { id: 'terminal', label: 'Terminal', icon: SquareTerminal }, diff --git a/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts b/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts index 2e3f784f..80ac4fae 100644 --- a/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts +++ b/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts @@ -3,6 +3,7 @@ import { useState, useCallback } from 'react'; export type SettingsViewId = | 'api-keys' | 'claude' + | 'providers' | 'ai-enhancement' | 'appearance' | 'terminal' diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx new file mode 100644 index 00000000..df69cc82 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx @@ -0,0 +1,35 @@ +import { useAppStore } from '@/store/app-store'; +import { useCliStatus } from '../hooks/use-cli-status'; +import { ClaudeCliStatus } from '../cli-status/claude-cli-status'; +import { ClaudeMdSettings } from '../claude/claude-md-settings'; +import { ClaudeUsageSection } from '../api-keys/claude-usage-section'; + +export function ClaudeSettingsTab() { + const { apiKeys, autoLoadClaudeMd, setAutoLoadClaudeMd } = useAppStore(); + + // Use CLI status hook + const { claudeCliStatus, isCheckingClaudeCli, handleRefreshClaudeCli } = useCliStatus(); + + // Hide usage tracking when using API key (only show for Claude Code CLI users) + // Also hide on Windows for now (CLI usage command not supported) + const isWindows = + typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win'); + const showUsageTracking = !apiKeys.anthropic && !isWindows; + + return ( +
+ + + {showUsageTracking && } +
+ ); +} + +export default ClaudeSettingsTab; diff --git a/apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx b/apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx new file mode 100644 index 00000000..56e51da1 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx @@ -0,0 +1,321 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Terminal, CheckCircle2, XCircle, Loader2, RefreshCw, ExternalLink } from 'lucide-react'; +import { toast } from 'sonner'; +import { getHttpApiClient } from '@/lib/http-api-client'; +import { useAppStore } from '@/store/app-store'; +import type { CursorModelId, CursorModelConfig, CursorCliConfig } from '@automaker/types'; +import { CURSOR_MODEL_MAP } from '@automaker/types'; + +interface CursorStatus { + installed: boolean; + version?: string; + authenticated: boolean; + method?: string; +} + +export function CursorSettingsTab() { + const { currentProject } = useAppStore(); + const [status, setStatus] = useState(null); + const [config, setConfig] = useState(null); + const [availableModels, setAvailableModels] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + + const loadData = useCallback(async () => { + setIsLoading(true); + try { + const api = getHttpApiClient(); + const statusResult = await api.setup.getCursorStatus(); + + if (statusResult.success) { + setStatus({ + installed: statusResult.installed ?? false, + version: statusResult.version ?? undefined, + authenticated: statusResult.auth?.authenticated ?? false, + method: statusResult.auth?.method, + }); + } + + // Only load config if we have a project path + if (currentProject?.path) { + const configResult = await api.setup.getCursorConfig(currentProject.path); + if (configResult.success) { + setConfig({ + defaultModel: configResult.config?.defaultModel as CursorModelId | undefined, + models: configResult.config?.models as CursorModelId[] | undefined, + mcpServers: configResult.config?.mcpServers, + rules: configResult.config?.rules, + }); + if (configResult.availableModels) { + setAvailableModels(configResult.availableModels as CursorModelConfig[]); + } else { + setAvailableModels(Object.values(CURSOR_MODEL_MAP)); + } + } else { + // Set defaults if no config + setAvailableModels(Object.values(CURSOR_MODEL_MAP)); + } + } else { + // No project, just show available models + setAvailableModels(Object.values(CURSOR_MODEL_MAP)); + } + } catch (error) { + console.error('Failed to load Cursor settings:', error); + toast.error('Failed to load Cursor settings'); + } finally { + setIsLoading(false); + } + }, [currentProject?.path]); + + useEffect(() => { + loadData(); + }, [loadData]); + + const handleDefaultModelChange = async (model: CursorModelId) => { + if (!currentProject?.path) { + toast.error('No project selected'); + return; + } + + setIsSaving(true); + try { + const api = getHttpApiClient(); + const result = await api.setup.setCursorDefaultModel(currentProject.path, model); + + if (result.success) { + setConfig((prev) => (prev ? { ...prev, defaultModel: model } : { defaultModel: model })); + toast.success('Default model updated'); + } else { + toast.error(result.error || 'Failed to update default model'); + } + } catch (error) { + toast.error('Failed to update default model'); + } finally { + setIsSaving(false); + } + }; + + const handleModelToggle = async (model: CursorModelId, enabled: boolean) => { + if (!currentProject?.path) { + toast.error('No project selected'); + return; + } + + const currentModels = config?.models || ['auto']; + const newModels = enabled + ? [...currentModels, model] + : currentModels.filter((m) => m !== model); + + setIsSaving(true); + try { + const api = getHttpApiClient(); + const result = await api.setup.setCursorModels(currentProject.path, newModels); + + if (result.success) { + setConfig((prev) => (prev ? { ...prev, models: newModels } : { models: newModels })); + } else { + toast.error(result.error || 'Failed to update models'); + } + } catch (error) { + toast.error('Failed to update models'); + } finally { + setIsSaving(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Status Card */} + + + + + Cursor CLI Status + + + + {/* Installation */} +
+ Installation + {status?.installed ? ( +
+ + v{status.version} +
+ ) : ( +
+ + Not installed +
+ )} +
+ + {/* Authentication */} +
+ Authentication + {status?.authenticated ? ( +
+ + + {status.method === 'api_key' ? 'API Key' : 'Browser Login'} + +
+ ) : ( +
+ + Not authenticated +
+ )} +
+ + {/* Actions */} +
+ + {!status?.installed && ( + + )} +
+
+
+ + {/* Model Configuration */} + {status?.installed && currentProject && ( + + + Model Configuration + + Configure which Cursor models are available and set the default + + + + {/* Default Model */} +
+ + +
+ + {/* Enabled Models */} +
+ +
+ {availableModels.map((model) => { + const isEnabled = config?.models?.includes(model.id) ?? model.id === 'auto'; + const isAuto = model.id === 'auto'; + + return ( +
+
+ handleModelToggle(model.id, !!checked)} + disabled={isSaving || isAuto} + /> +
+
+ {model.label} + {model.hasThinking && ( + + Thinking + + )} +
+

{model.description}

+
+
+ + {model.tier} + +
+ ); + })} +
+
+
+
+ )} + + {/* Not Installed State */} + {!status?.installed && ( + + + +

Cursor CLI is not installed.

+

Install it to use Cursor models in AutoMaker.

+
+
+ )} + + {/* No Project Selected */} + {status?.installed && !currentProject && ( + + + +

No project selected.

+

Select a project to configure Cursor models.

+
+
+ )} +
+ ); +} + +export default CursorSettingsTab; diff --git a/apps/ui/src/components/views/settings-view/providers/index.ts b/apps/ui/src/components/views/settings-view/providers/index.ts new file mode 100644 index 00000000..c9284867 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/index.ts @@ -0,0 +1,3 @@ +export { ProviderTabs } from './provider-tabs'; +export { ClaudeSettingsTab } from './claude-settings-tab'; +export { CursorSettingsTab } from './cursor-settings-tab'; diff --git a/apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx b/apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx new file mode 100644 index 00000000..dc97cf2f --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Bot, Terminal } from 'lucide-react'; +import { CursorSettingsTab } from './cursor-settings-tab'; +import { ClaudeSettingsTab } from './claude-settings-tab'; + +interface ProviderTabsProps { + defaultTab?: 'claude' | 'cursor'; +} + +export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) { + return ( + + + + + Claude + + + + Cursor + + + + + + + + + + + + ); +} + +export default ProviderTabs; diff --git a/plan/cursor-cli-integration/README.md b/plan/cursor-cli-integration/README.md index 6fad613f..4e1fd7a1 100644 --- a/plan/cursor-cli-integration/README.md +++ b/plan/cursor-cli-integration/README.md @@ -13,7 +13,7 @@ | 4 | [Setup Routes & Status Endpoints](phases/phase-4-routes.md) | `completed` | ✅ | | 5 | [Log Parser Integration](phases/phase-5-log-parser.md) | `completed` | ✅ | | 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `completed` | ✅ | -| 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `pending` | - | +| 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `completed` | ✅ | | 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `pending` | - | | 9 | [Task Execution Integration](phases/phase-9-execution.md) | `pending` | - | | 10 | [Testing & Validation](phases/phase-10-testing.md) | `pending` | - | diff --git a/plan/cursor-cli-integration/phases/phase-7-settings.md b/plan/cursor-cli-integration/phases/phase-7-settings.md index ecbad2b2..46255fef 100644 --- a/plan/cursor-cli-integration/phases/phase-7-settings.md +++ b/plan/cursor-cli-integration/phases/phase-7-settings.md @@ -1,6 +1,6 @@ # Phase 7: Settings View Provider Tabs -**Status:** `pending` +**Status:** `completed` **Dependencies:** Phase 4 (Routes) **Estimated Effort:** Medium (React components) @@ -16,7 +16,7 @@ Create a tabbed interface in Settings for managing different AI providers (Claud ### Task 7.1: Create Cursor Settings Tab Component -**Status:** `pending` +**Status:** `completed` **File:** `apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx` @@ -311,7 +311,7 @@ export default CursorSettingsTab; ### Task 7.2: Create Provider Tabs Container -**Status:** `pending` +**Status:** `completed` **File:** `apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx` @@ -356,7 +356,7 @@ export default ProviderTabs; ### Task 7.3: Create Claude Settings Tab (if not exists) -**Status:** `pending` +**Status:** `completed` **File:** `apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx` @@ -461,7 +461,7 @@ export default ClaudeSettingsTab; ### Task 7.4: Update Settings View Navigation -**Status:** `pending` +**Status:** `completed` **File:** `apps/ui/src/components/views/settings-view/config/navigation.ts` @@ -482,7 +482,7 @@ export const SETTINGS_NAVIGATION = [ ### Task 7.5: Integrate Provider Tabs in Settings -**Status:** `pending` +**Status:** `completed` Update the settings view to render ProviderTabs for the providers section.