diff --git a/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx index cf7b842d..ebcec5ab 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx @@ -63,6 +63,91 @@ export function CursorCliStatusSkeleton() { ); } +export function CursorPermissionsSkeleton() { + return ( +
+
+
+ +
+ + +
+
+
+ + +
+
+
+ {/* Security Warning skeleton */} +
+ +
+ + + +
+
+ {/* Permission Profiles skeleton */} +
+ +
+ {[1, 2].map((i) => ( +
+
+
+
+ + + +
+ + +
+ + + +
+
+
+ + +
+
+
+ ))} +
+
+ {/* Config File Locations skeleton */} +
+ +
+
+
+ + +
+ +
+
+ + +
+
+
+
+
+ ); +} + export function ModelConfigSkeleton() { return (
; +} + +/** + * Custom hook for managing Cursor CLI permissions + * Handles loading permissions data, applying profiles, and copying configs + */ +export function useCursorPermissions(projectPath?: string) { + const [permissions, setPermissions] = useState(null); + const [isLoadingPermissions, setIsLoadingPermissions] = useState(false); + const [isSavingPermissions, setIsSavingPermissions] = useState(false); + const [copiedConfig, setCopiedConfig] = useState(false); + + // Load permissions data + const loadPermissions = useCallback(async () => { + setIsLoadingPermissions(true); + try { + const api = getHttpApiClient(); + const result = await api.setup.getCursorPermissions(projectPath); + + if (result.success) { + setPermissions({ + activeProfile: result.activeProfile || null, + effectivePermissions: result.effectivePermissions || null, + hasProjectConfig: result.hasProjectConfig || false, + availableProfiles: result.availableProfiles || [], + }); + } + } catch (error) { + console.error('Failed to load Cursor permissions:', error); + } finally { + setIsLoadingPermissions(false); + } + }, [projectPath]); + + // Apply a permission profile + const applyProfile = useCallback( + async (profileId: 'strict' | 'development', scope: 'global' | 'project') => { + setIsSavingPermissions(true); + try { + const api = getHttpApiClient(); + const result = await api.setup.applyCursorPermissionProfile( + profileId, + scope, + scope === 'project' ? projectPath : undefined + ); + + if (result.success) { + toast.success(result.message || `Applied ${profileId} profile`); + await loadPermissions(); + } else { + toast.error(result.error || 'Failed to apply profile'); + } + } catch (error) { + toast.error('Failed to apply profile'); + } finally { + setIsSavingPermissions(false); + } + }, + [projectPath, loadPermissions] + ); + + // Copy example config to clipboard + const copyConfig = useCallback(async (profileId: 'strict' | 'development') => { + try { + const api = getHttpApiClient(); + const result = await api.setup.getCursorExampleConfig(profileId); + + if (result.success && result.config) { + await navigator.clipboard.writeText(result.config); + setCopiedConfig(true); + toast.success('Config copied to clipboard'); + setTimeout(() => setCopiedConfig(false), 2000); + } + } catch (error) { + toast.error('Failed to copy config'); + } + }, []); + + return { + permissions, + isLoadingPermissions, + isSavingPermissions, + copiedConfig, + loadPermissions, + applyProfile, + copyConfig, + }; +} diff --git a/apps/ui/src/components/views/settings-view/hooks/use-cursor-status.ts b/apps/ui/src/components/views/settings-view/hooks/use-cursor-status.ts new file mode 100644 index 00000000..a9d20788 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/hooks/use-cursor-status.ts @@ -0,0 +1,67 @@ +import { useState, useEffect, useCallback } from 'react'; +import { toast } from 'sonner'; +import { getHttpApiClient } from '@/lib/http-api-client'; +import { useSetupStore } from '@/store/setup-store'; + +export interface CursorStatus { + installed: boolean; + version?: string; + authenticated: boolean; + method?: string; +} + +/** + * Custom hook for managing Cursor CLI status + * Handles checking CLI installation, authentication, and refresh functionality + */ +export function useCursorStatus() { + const { setCursorCliStatus } = useSetupStore(); + + const [status, setStatus] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const loadData = useCallback(async () => { + setIsLoading(true); + try { + const api = getHttpApiClient(); + const statusResult = await api.setup.getCursorStatus(); + + if (statusResult.success) { + const newStatus = { + installed: statusResult.installed ?? false, + version: statusResult.version ?? undefined, + authenticated: statusResult.auth?.authenticated ?? false, + method: statusResult.auth?.method, + }; + setStatus(newStatus); + + // Also update the global setup store so other components can access the status + setCursorCliStatus({ + installed: newStatus.installed, + version: newStatus.version, + auth: newStatus.authenticated + ? { + authenticated: true, + method: newStatus.method || 'unknown', + } + : undefined, + }); + } + } catch (error) { + console.error('Failed to load Cursor settings:', error); + toast.error('Failed to load Cursor settings'); + } finally { + setIsLoading(false); + } + }, [setCursorCliStatus]); + + useEffect(() => { + loadData(); + }, [loadData]); + + return { + status, + isLoading, + loadData, + }; +} diff --git a/apps/ui/src/components/views/settings-view/providers/cursor-model-configuration.tsx b/apps/ui/src/components/views/settings-view/providers/cursor-model-configuration.tsx new file mode 100644 index 00000000..a4758764 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/cursor-model-configuration.tsx @@ -0,0 +1,131 @@ +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 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { CursorModelId, CursorModelConfig } from '@automaker/types'; +import { CURSOR_MODEL_MAP } from '@automaker/types'; + +interface CursorModelConfigurationProps { + enabledCursorModels: CursorModelId[]; + cursorDefaultModel: CursorModelId; + isSaving: boolean; + onDefaultModelChange: (model: CursorModelId) => void; + onModelToggle: (model: CursorModelId, enabled: boolean) => void; +} + +export function CursorModelConfiguration({ + enabledCursorModels, + cursorDefaultModel, + isSaving, + onDefaultModelChange, + onModelToggle, +}: CursorModelConfigurationProps) { + // All available models from the model map + const availableModels: CursorModelConfig[] = Object.values(CURSOR_MODEL_MAP); + + return ( +
+
+
+
+ +
+

+ Model Configuration +

+
+

+ Configure which Cursor models are available in the feature modal +

+
+
+ {/* Default Model */} +
+ + +
+ + {/* Enabled Models */} +
+ +
+ {availableModels.map((model) => { + const isEnabled = enabledCursorModels.includes(model.id); + const isAuto = model.id === 'auto'; + + return ( +
+
+ onModelToggle(model.id, !!checked)} + disabled={isSaving || isAuto} + /> +
+
+ {model.label} + {model.hasThinking && ( + + Thinking + + )} +
+

{model.description}

+
+
+ + {model.tier} + +
+ ); + })} +
+
+
+
+ ); +} diff --git a/apps/ui/src/components/views/settings-view/providers/cursor-permissions-section.tsx b/apps/ui/src/components/views/settings-view/providers/cursor-permissions-section.tsx new file mode 100644 index 00000000..29be25b3 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/cursor-permissions-section.tsx @@ -0,0 +1,253 @@ +import { useState, useEffect } from 'react'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { Shield, ShieldCheck, ShieldAlert, ChevronDown, Copy, Check } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { CursorStatus } from '../hooks/use-cursor-status'; +import type { PermissionsData } from '../hooks/use-cursor-permissions'; + +interface CursorPermissionsSectionProps { + status: CursorStatus | null; + permissions: PermissionsData | null; + isLoadingPermissions: boolean; + isSavingPermissions: boolean; + copiedConfig: boolean; + currentProject?: { path: string } | null; + onApplyProfile: ( + profileId: 'strict' | 'development', + scope: 'global' | 'project' + ) => Promise; + onCopyConfig: (profileId: 'strict' | 'development') => Promise; + onLoadPermissions: () => Promise; +} + +export function CursorPermissionsSection({ + status, + permissions, + isLoadingPermissions, + isSavingPermissions, + copiedConfig, + currentProject, + onApplyProfile, + onCopyConfig, + onLoadPermissions, +}: CursorPermissionsSectionProps) { + const [permissionsExpanded, setPermissionsExpanded] = useState(false); + + // Load permissions when section is expanded + useEffect(() => { + if (permissionsExpanded && status?.installed && !permissions) { + onLoadPermissions(); + } + }, [permissionsExpanded, status?.installed, permissions, onLoadPermissions]); + + if (!status?.installed) { + return null; + } + + return ( + +
+ +
+
+
+ +
+
+

+ CLI Permissions +

+

Configure what Cursor CLI can do

+
+
+
+ {permissions?.activeProfile && ( + + {permissions.activeProfile === 'strict' && ( + + )} + {permissions.activeProfile === 'development' && ( + + )} + {permissions.activeProfile} + + )} + +
+
+
+ + +
+ {/* Security Warning */} +
+ +
+ Security Notice +

+ Cursor CLI can execute shell commands based on its permission config. For + overnight automation, consider using the Strict profile to limit what commands can + run. +

+
+
+ + {isLoadingPermissions ? ( +
+
+
+ ) : ( + <> + {/* Permission Profiles */} +
+ +
+ {permissions?.availableProfiles.map((profile) => ( +
+
+
+
+ {profile.id === 'strict' ? ( + + ) : ( + + )} + {profile.name} + {permissions.activeProfile === profile.id && ( + + Active + + )} +
+

+ {profile.description} +

+
+ + {profile.permissions.allow.length} allowed + + | + + {profile.permissions.deny.length} denied + +
+
+
+ + {currentProject && ( + + )} +
+
+
+ ))} +
+
+ + {/* Config File Location */} +
+ +
+
+
+

Global Config

+

+ ~/.cursor/cli-config.json +

+
+ +
+
+

Project Config

+

+ <project>/.cursor/cli.json +

+ {permissions?.hasProjectConfig && ( + + Project override active + + )} +
+
+
+ + {/* Documentation Link */} +
+ Learn more about{' '} + + Cursor CLI permissions + +
+ + )} +
+ +
+ + ); +} 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 index f20d8bfc..c9b77375 100644 --- 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 @@ -1,57 +1,17 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Label } from '@/components/ui/label'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; -import { - Terminal, - Info, - Shield, - ShieldCheck, - ShieldAlert, - ChevronDown, - Copy, - Check, -} from 'lucide-react'; +import { useState } from 'react'; import { toast } from 'sonner'; -import { getHttpApiClient } from '@/lib/http-api-client'; import { useAppStore } from '@/store/app-store'; -import { useSetupStore } from '@/store/setup-store'; -import { cn } from '@/lib/utils'; -import type { CursorModelId, CursorModelConfig, CursorPermissionProfile } from '@automaker/types'; -import { CURSOR_MODEL_MAP } from '@automaker/types'; +import type { CursorModelId } from '@automaker/types'; import { CursorCliStatus, CursorCliStatusSkeleton, + CursorPermissionsSkeleton, ModelConfigSkeleton, } from '../cli-status/cursor-cli-status'; - -interface CursorStatus { - installed: boolean; - version?: string; - authenticated: boolean; - method?: string; -} - -interface PermissionsData { - activeProfile: CursorPermissionProfile | null; - effectivePermissions: { allow: string[]; deny: string[] } | null; - hasProjectConfig: boolean; - availableProfiles: Array<{ - id: string; - name: string; - description: string; - permissions: { allow: string[]; deny: string[] }; - }>; -} +import { useCursorStatus } from '../hooks/use-cursor-status'; +import { useCursorPermissions } from '../hooks/use-cursor-permissions'; +import { CursorPermissionsSection } from './cursor-permissions-section'; +import { CursorModelConfiguration } from './cursor-model-configuration'; export function CursorSettingsTab() { // Global settings from store @@ -62,61 +22,22 @@ export function CursorSettingsTab() { toggleCursorModel, currentProject, } = useAppStore(); - const { setCursorCliStatus } = useSetupStore(); - const [status, setStatus] = useState(null); - const [isLoading, setIsLoading] = useState(true); + // Custom hooks for data fetching + const { status, isLoading, loadData } = useCursorStatus(); + const { + permissions, + isLoadingPermissions, + isSavingPermissions, + copiedConfig, + loadPermissions, + applyProfile, + copyConfig, + } = useCursorPermissions(currentProject?.path); + + // Local state for model configuration saving const [isSaving, setIsSaving] = useState(false); - // Permissions state - const [permissions, setPermissions] = useState(null); - const [isLoadingPermissions, setIsLoadingPermissions] = useState(false); - const [isSavingPermissions, setIsSavingPermissions] = useState(false); - const [permissionsExpanded, setPermissionsExpanded] = useState(false); - const [copiedConfig, setCopiedConfig] = useState(false); - - // All available models from the model map - const availableModels: CursorModelConfig[] = Object.values(CURSOR_MODEL_MAP); - - const loadData = useCallback(async () => { - setIsLoading(true); - try { - const api = getHttpApiClient(); - const statusResult = await api.setup.getCursorStatus(); - - if (statusResult.success) { - const newStatus = { - installed: statusResult.installed ?? false, - version: statusResult.version ?? undefined, - authenticated: statusResult.auth?.authenticated ?? false, - method: statusResult.auth?.method, - }; - setStatus(newStatus); - - // Also update the global setup store so other components can access the status - setCursorCliStatus({ - installed: newStatus.installed, - version: newStatus.version, - auth: newStatus.authenticated - ? { - authenticated: true, - method: newStatus.method || 'unknown', - } - : undefined, - }); - } - } catch (error) { - console.error('Failed to load Cursor settings:', error); - toast.error('Failed to load Cursor settings'); - } finally { - setIsLoading(false); - } - }, [setCursorCliStatus]); - - useEffect(() => { - loadData(); - }, [loadData]); - const handleDefaultModelChange = (model: CursorModelId) => { setIsSaving(true); try { @@ -140,93 +61,11 @@ export function CursorSettingsTab() { } }; - // Load permissions data - const loadPermissions = useCallback(async () => { - setIsLoadingPermissions(true); - try { - const api = getHttpApiClient(); - const result = await api.setup.getCursorPermissions(currentProject?.path); - - if (result.success) { - setPermissions({ - activeProfile: result.activeProfile || null, - effectivePermissions: result.effectivePermissions || null, - hasProjectConfig: result.hasProjectConfig || false, - availableProfiles: result.availableProfiles || [], - }); - } - } catch (error) { - console.error('Failed to load Cursor permissions:', error); - } finally { - setIsLoadingPermissions(false); - } - }, [currentProject?.path]); - - // Load permissions when tab is expanded - useEffect(() => { - if (permissionsExpanded && status?.installed && !permissions) { - loadPermissions(); - } - }, [permissionsExpanded, status?.installed, permissions, loadPermissions]); - - // Apply a permission profile - const handleApplyProfile = async ( - profileId: 'strict' | 'development', - scope: 'global' | 'project' - ) => { - setIsSavingPermissions(true); - try { - const api = getHttpApiClient(); - const result = await api.setup.applyCursorPermissionProfile( - profileId, - scope, - scope === 'project' ? currentProject?.path : undefined - ); - - if (result.success) { - toast.success(result.message || `Applied ${profileId} profile`); - await loadPermissions(); - } else { - toast.error(result.error || 'Failed to apply profile'); - } - } catch (error) { - toast.error('Failed to apply profile'); - } finally { - setIsSavingPermissions(false); - } - }; - - // Copy example config to clipboard - const handleCopyConfig = async (profileId: 'strict' | 'development') => { - try { - const api = getHttpApiClient(); - const result = await api.setup.getCursorExampleConfig(profileId); - - if (result.success && result.config) { - await navigator.clipboard.writeText(result.config); - setCopiedConfig(true); - toast.success('Config copied to clipboard'); - setTimeout(() => setCopiedConfig(false), 2000); - } - } catch (error) { - toast.error('Failed to copy config'); - } - }; - if (isLoading) { return (
- {/* Usage Info skeleton */} -
- -
- Board View Only -

- Cursor is currently only available for the Kanban board agent tasks. -

-
-
+
); @@ -234,336 +73,31 @@ export function CursorSettingsTab() { return (
- {/* Usage Info */} -
- -
- Board View Only -

- Cursor is currently only available for the Kanban board agent tasks. -

-
-
- {/* CLI Status */} + {/* CLI Permissions Section */} + + {/* Model Configuration - Always show (global settings) */} {status?.installed && ( -
-
-
-
- -
-

- Model Configuration -

-
-

- Configure which Cursor models are available in the feature modal -

-
-
- {/* Default Model */} -
- - -
- - {/* Enabled Models */} -
- -
- {availableModels.map((model) => { - const isEnabled = enabledCursorModels.includes(model.id); - const isAuto = model.id === 'auto'; - - return ( -
-
- handleModelToggle(model.id, !!checked)} - disabled={isSaving || isAuto} - /> -
-
- {model.label} - {model.hasThinking && ( - - Thinking - - )} -
-

{model.description}

-
-
- - {model.tier} - -
- ); - })} -
-
-
-
- )} - - {/* CLI Permissions Section */} - {status?.installed && ( - -
- -
-
-
- -
-
-

- CLI Permissions -

-

- Configure what Cursor CLI can do -

-
-
-
- {permissions?.activeProfile && ( - - {permissions.activeProfile === 'strict' && ( - - )} - {permissions.activeProfile === 'development' && ( - - )} - {permissions.activeProfile} - - )} - -
-
-
- - -
- {/* Security Warning */} -
- -
- Security Notice -

- Cursor CLI can execute shell commands based on its permission config. For - overnight automation, consider using the Strict profile to limit what commands - can run. -

-
-
- - {isLoadingPermissions ? ( -
-
-
- ) : ( - <> - {/* Permission Profiles */} -
- -
- {permissions?.availableProfiles.map((profile) => ( -
-
-
-
- {profile.id === 'strict' ? ( - - ) : ( - - )} - {profile.name} - {permissions.activeProfile === profile.id && ( - - Active - - )} -
-

- {profile.description} -

-
- - {profile.permissions.allow.length} allowed - - | - - {profile.permissions.deny.length} denied - -
-
-
- - {currentProject && ( - - )} -
-
-
- ))} -
-
- - {/* Config File Location */} -
- -
-
-
-

Global Config

-

- ~/.cursor/cli-config.json -

-
- -
-
-

Project Config

-

- <project>/.cursor/cli.json -

- {permissions?.hasProjectConfig && ( - - Project override active - - )} -
-
-
- - {/* Documentation Link */} -
- Learn more about{' '} - - Cursor CLI permissions - -
- - )} -
- -
- + )}
);