mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
refactor(cursor): seperate components and add permissions skeleton
This commit is contained in:
@@ -63,6 +63,91 @@ export function CursorCliStatusSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CursorPermissionsSkeleton() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SkeletonPulse className="w-9 h-9 rounded-xl" />
|
||||||
|
<div className="text-left">
|
||||||
|
<SkeletonPulse className="h-6 w-32 mb-2" />
|
||||||
|
<SkeletonPulse className="h-4 w-48" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SkeletonPulse className="h-6 w-20 rounded-full" />
|
||||||
|
<SkeletonPulse className="w-5 h-5 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Security Warning skeleton */}
|
||||||
|
<div className="flex items-start gap-3 p-4 rounded-xl border border-border/30 bg-muted/10">
|
||||||
|
<SkeletonPulse className="w-5 h-5 rounded shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<SkeletonPulse className="h-4 w-32" />
|
||||||
|
<SkeletonPulse className="h-3 w-full" />
|
||||||
|
<SkeletonPulse className="h-3 w-3/4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Permission Profiles skeleton */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SkeletonPulse className="h-4 w-36" />
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<div key={i} className="p-4 rounded-xl border border-border/30 bg-muted/10">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SkeletonPulse className="w-4 h-4 rounded" />
|
||||||
|
<SkeletonPulse className="h-4 w-24" />
|
||||||
|
<SkeletonPulse className="h-4 w-12 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<SkeletonPulse className="h-3 w-full" />
|
||||||
|
<SkeletonPulse className="h-3 w-2/3" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SkeletonPulse className="h-3 w-20" />
|
||||||
|
<SkeletonPulse className="h-3 w-1" />
|
||||||
|
<SkeletonPulse className="h-3 w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<SkeletonPulse className="h-8 w-28 rounded-md" />
|
||||||
|
<SkeletonPulse className="h-8 w-28 rounded-md" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Config File Locations skeleton */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SkeletonPulse className="h-4 w-40" />
|
||||||
|
<div className="p-4 rounded-xl border border-border/30 bg-muted/10 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<SkeletonPulse className="h-4 w-24" />
|
||||||
|
<SkeletonPulse className="h-3 w-48" />
|
||||||
|
</div>
|
||||||
|
<SkeletonPulse className="w-8 h-8 rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-border/30 pt-2 space-y-1">
|
||||||
|
<SkeletonPulse className="h-4 w-28" />
|
||||||
|
<SkeletonPulse className="h-3 w-40" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ModelConfigSkeleton() {
|
export function ModelConfigSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export { useCliStatus } from './use-cli-status';
|
export { useCliStatus } from './use-cli-status';
|
||||||
export { useSettingsView, type SettingsViewId } from './use-settings-view';
|
export { useSettingsView, type SettingsViewId } from './use-settings-view';
|
||||||
|
export { useCursorStatus, type CursorStatus } from './use-cursor-status';
|
||||||
|
export { useCursorPermissions, type PermissionsData } from './use-cursor-permissions';
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
|
import type { CursorPermissionProfile } from '@automaker/types';
|
||||||
|
|
||||||
|
export 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[] };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<PermissionsData | null>(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<CursorStatus | null>(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||||
|
<Terminal className="w-5 h-5 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||||
|
Model Configuration
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||||
|
Configure which Cursor models are available in the feature modal
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Default Model */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Default Model</Label>
|
||||||
|
<Select
|
||||||
|
value={cursorDefaultModel}
|
||||||
|
onValueChange={(v) => onDefaultModelChange(v as CursorModelId)}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{enabledCursorModels.map((modelId) => {
|
||||||
|
const model = CURSOR_MODEL_MAP[modelId];
|
||||||
|
if (!model) return null;
|
||||||
|
return (
|
||||||
|
<SelectItem key={modelId} value={modelId}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{model.label}</span>
|
||||||
|
{model.hasThinking && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Thinking
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enabled Models */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>Available Models</Label>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{availableModels.map((model) => {
|
||||||
|
const isEnabled = enabledCursorModels.includes(model.id);
|
||||||
|
const isAuto = model.id === 'auto';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={model.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-xl border border-border/50 bg-card/50 hover:bg-accent/30 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={isEnabled}
|
||||||
|
onCheckedChange={(checked) => onModelToggle(model.id, !!checked)}
|
||||||
|
disabled={isSaving || isAuto}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">{model.label}</span>
|
||||||
|
{model.hasThinking && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Thinking
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{model.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant={model.tier === 'free' ? 'default' : 'secondary'}>
|
||||||
|
{model.tier}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<void>;
|
||||||
|
onCopyConfig: (profileId: 'strict' | 'development') => Promise<void>;
|
||||||
|
onLoadPermissions: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Collapsible open={permissionsExpanded} onOpenChange={setPermissionsExpanded}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger className="w-full">
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-500/20 to-amber-600/10 flex items-center justify-center border border-amber-500/20">
|
||||||
|
<Shield className="w-5 h-5 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||||
|
CLI Permissions
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground/80">Configure what Cursor CLI can do</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{permissions?.activeProfile && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
permissions.activeProfile === 'strict'
|
||||||
|
? 'border-green-500/50 text-green-500'
|
||||||
|
: permissions.activeProfile === 'development'
|
||||||
|
? 'border-blue-500/50 text-blue-500'
|
||||||
|
: 'border-amber-500/50 text-amber-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{permissions.activeProfile === 'strict' && (
|
||||||
|
<ShieldCheck className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{permissions.activeProfile === 'development' && (
|
||||||
|
<ShieldAlert className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{permissions.activeProfile}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
'w-5 h-5 text-muted-foreground transition-transform',
|
||||||
|
permissionsExpanded && 'rotate-180'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Security Warning */}
|
||||||
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
||||||
|
<ShieldAlert className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-amber-400/90">
|
||||||
|
<span className="font-medium">Security Notice</span>
|
||||||
|
<p className="text-xs text-amber-400/70 mt-1">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingPermissions ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin w-6 h-6 border-2 border-brand-500 border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Permission Profiles */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>Permission Profiles</Label>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{permissions?.availableProfiles.map((profile) => (
|
||||||
|
<div
|
||||||
|
key={profile.id}
|
||||||
|
className={cn(
|
||||||
|
'p-4 rounded-xl border transition-colors',
|
||||||
|
permissions.activeProfile === profile.id
|
||||||
|
? 'border-brand-500/50 bg-brand-500/5'
|
||||||
|
: 'border-border/50 bg-card/50 hover:bg-accent/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
{profile.id === 'strict' ? (
|
||||||
|
<ShieldCheck className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<ShieldAlert className="w-4 h-4 text-blue-500" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium">{profile.name}</span>
|
||||||
|
{permissions.activeProfile === profile.id && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
{profile.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span className="text-green-500">
|
||||||
|
{profile.permissions.allow.length} allowed
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground/50">|</span>
|
||||||
|
<span className="text-red-500">
|
||||||
|
{profile.permissions.deny.length} denied
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={
|
||||||
|
permissions.activeProfile === profile.id ? 'secondary' : 'default'
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
isSavingPermissions || permissions.activeProfile === profile.id
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
onApplyProfile(profile.id as 'strict' | 'development', 'global')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Apply Globally
|
||||||
|
</Button>
|
||||||
|
{currentProject && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isSavingPermissions}
|
||||||
|
onClick={() =>
|
||||||
|
onApplyProfile(profile.id as 'strict' | 'development', 'project')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Apply to Project
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Config File Location */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>Config File Locations</Label>
|
||||||
|
<div className="p-4 rounded-xl border border-border/50 bg-card/30 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Global Config</p>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
~/.cursor/cli-config.json
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="ghost" onClick={() => onCopyConfig('development')}>
|
||||||
|
{copiedConfig ? (
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-border/30 pt-2">
|
||||||
|
<p className="text-sm font-medium">Project Config</p>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
<project>/.cursor/cli.json
|
||||||
|
</p>
|
||||||
|
{permissions?.hasProjectConfig && (
|
||||||
|
<Badge variant="secondary" className="mt-1 text-xs">
|
||||||
|
Project override active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Documentation Link */}
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Learn more about{' '}
|
||||||
|
<a
|
||||||
|
href="https://cursor.com/docs/cli/reference/permissions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-brand-500 hover:underline"
|
||||||
|
>
|
||||||
|
Cursor CLI permissions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,57 +1,17 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState } 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 { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import type { CursorModelId } from '@automaker/types';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import type { CursorModelId, CursorModelConfig, CursorPermissionProfile } from '@automaker/types';
|
|
||||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
|
||||||
import {
|
import {
|
||||||
CursorCliStatus,
|
CursorCliStatus,
|
||||||
CursorCliStatusSkeleton,
|
CursorCliStatusSkeleton,
|
||||||
|
CursorPermissionsSkeleton,
|
||||||
ModelConfigSkeleton,
|
ModelConfigSkeleton,
|
||||||
} from '../cli-status/cursor-cli-status';
|
} from '../cli-status/cursor-cli-status';
|
||||||
|
import { useCursorStatus } from '../hooks/use-cursor-status';
|
||||||
interface CursorStatus {
|
import { useCursorPermissions } from '../hooks/use-cursor-permissions';
|
||||||
installed: boolean;
|
import { CursorPermissionsSection } from './cursor-permissions-section';
|
||||||
version?: string;
|
import { CursorModelConfiguration } from './cursor-model-configuration';
|
||||||
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[] };
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CursorSettingsTab() {
|
export function CursorSettingsTab() {
|
||||||
// Global settings from store
|
// Global settings from store
|
||||||
@@ -62,61 +22,22 @@ export function CursorSettingsTab() {
|
|||||||
toggleCursorModel,
|
toggleCursorModel,
|
||||||
currentProject,
|
currentProject,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const { setCursorCliStatus } = useSetupStore();
|
|
||||||
|
|
||||||
const [status, setStatus] = useState<CursorStatus | null>(null);
|
// Custom hooks for data fetching
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
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);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
// Permissions state
|
|
||||||
const [permissions, setPermissions] = useState<PermissionsData | null>(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) => {
|
const handleDefaultModelChange = (model: CursorModelId) => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Usage Info skeleton */}
|
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
|
||||||
<Info className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
|
|
||||||
<div className="text-sm text-amber-400/90">
|
|
||||||
<span className="font-medium">Board View Only</span>
|
|
||||||
<p className="text-xs text-amber-400/70 mt-1">
|
|
||||||
Cursor is currently only available for the Kanban board agent tasks.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<CursorCliStatusSkeleton />
|
<CursorCliStatusSkeleton />
|
||||||
|
<CursorPermissionsSkeleton />
|
||||||
<ModelConfigSkeleton />
|
<ModelConfigSkeleton />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -234,336 +73,31 @@ export function CursorSettingsTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Usage Info */}
|
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
|
||||||
<Info className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
|
|
||||||
<div className="text-sm text-amber-400/90">
|
|
||||||
<span className="font-medium">Board View Only</span>
|
|
||||||
<p className="text-xs text-amber-400/70 mt-1">
|
|
||||||
Cursor is currently only available for the Kanban board agent tasks.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CLI Status */}
|
{/* CLI Status */}
|
||||||
<CursorCliStatus status={status} isChecking={isLoading} onRefresh={loadData} />
|
<CursorCliStatus status={status} isChecking={isLoading} onRefresh={loadData} />
|
||||||
|
|
||||||
|
{/* CLI Permissions Section */}
|
||||||
|
<CursorPermissionsSection
|
||||||
|
status={status}
|
||||||
|
permissions={permissions}
|
||||||
|
isLoadingPermissions={isLoadingPermissions}
|
||||||
|
isSavingPermissions={isSavingPermissions}
|
||||||
|
copiedConfig={copiedConfig}
|
||||||
|
currentProject={currentProject}
|
||||||
|
onApplyProfile={applyProfile}
|
||||||
|
onCopyConfig={copyConfig}
|
||||||
|
onLoadPermissions={loadPermissions}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Model Configuration - Always show (global settings) */}
|
{/* Model Configuration - Always show (global settings) */}
|
||||||
{status?.installed && (
|
{status?.installed && (
|
||||||
<div
|
<CursorModelConfiguration
|
||||||
className={cn(
|
enabledCursorModels={enabledCursorModels}
|
||||||
'rounded-2xl overflow-hidden',
|
cursorDefaultModel={cursorDefaultModel}
|
||||||
'border border-border/50',
|
isSaving={isSaving}
|
||||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
onDefaultModelChange={handleDefaultModelChange}
|
||||||
'shadow-sm shadow-black/5'
|
onModelToggle={handleModelToggle}
|
||||||
)}
|
/>
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
|
||||||
<Terminal className="w-5 h-5 text-brand-500" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
|
||||||
Model Configuration
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
|
||||||
Configure which Cursor models are available in the feature modal
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* Default Model */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Default Model</Label>
|
|
||||||
<Select
|
|
||||||
value={cursorDefaultModel}
|
|
||||||
onValueChange={(v) => handleDefaultModelChange(v as CursorModelId)}
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{enabledCursorModels.map((modelId) => {
|
|
||||||
const model = CURSOR_MODEL_MAP[modelId];
|
|
||||||
if (!model) return null;
|
|
||||||
return (
|
|
||||||
<SelectItem key={modelId} value={modelId}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>{model.label}</span>
|
|
||||||
{model.hasThinking && (
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
Thinking
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enabled Models */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label>Available Models</Label>
|
|
||||||
<div className="grid gap-3">
|
|
||||||
{availableModels.map((model) => {
|
|
||||||
const isEnabled = enabledCursorModels.includes(model.id);
|
|
||||||
const isAuto = model.id === 'auto';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={model.id}
|
|
||||||
className="flex items-center justify-between p-3 rounded-xl border border-border/50 bg-card/50 hover:bg-accent/30 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Checkbox
|
|
||||||
checked={isEnabled}
|
|
||||||
onCheckedChange={(checked) => handleModelToggle(model.id, !!checked)}
|
|
||||||
disabled={isSaving || isAuto}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm font-medium">{model.label}</span>
|
|
||||||
{model.hasThinking && (
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
Thinking
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">{model.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant={model.tier === 'free' ? 'default' : 'secondary'}>
|
|
||||||
{model.tier}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* CLI Permissions Section */}
|
|
||||||
{status?.installed && (
|
|
||||||
<Collapsible open={permissionsExpanded} onOpenChange={setPermissionsExpanded}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'rounded-2xl overflow-hidden',
|
|
||||||
'border border-border/50',
|
|
||||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
|
||||||
'shadow-sm shadow-black/5'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CollapsibleTrigger className="w-full">
|
|
||||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-500/20 to-amber-600/10 flex items-center justify-center border border-amber-500/20">
|
|
||||||
<Shield className="w-5 h-5 text-amber-500" />
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
|
||||||
CLI Permissions
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground/80">
|
|
||||||
Configure what Cursor CLI can do
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{permissions?.activeProfile && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
permissions.activeProfile === 'strict'
|
|
||||||
? 'border-green-500/50 text-green-500'
|
|
||||||
: permissions.activeProfile === 'development'
|
|
||||||
? 'border-blue-500/50 text-blue-500'
|
|
||||||
: 'border-amber-500/50 text-amber-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{permissions.activeProfile === 'strict' && (
|
|
||||||
<ShieldCheck className="w-3 h-3 mr-1" />
|
|
||||||
)}
|
|
||||||
{permissions.activeProfile === 'development' && (
|
|
||||||
<ShieldAlert className="w-3 h-3 mr-1" />
|
|
||||||
)}
|
|
||||||
{permissions.activeProfile}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<ChevronDown
|
|
||||||
className={cn(
|
|
||||||
'w-5 h-5 text-muted-foreground transition-transform',
|
|
||||||
permissionsExpanded && 'rotate-180'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
|
|
||||||
<CollapsibleContent>
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* Security Warning */}
|
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
|
||||||
<ShieldAlert className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
|
|
||||||
<div className="text-sm text-amber-400/90">
|
|
||||||
<span className="font-medium">Security Notice</span>
|
|
||||||
<p className="text-xs text-amber-400/70 mt-1">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoadingPermissions ? (
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
<div className="animate-spin w-6 h-6 border-2 border-brand-500 border-t-transparent rounded-full" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* Permission Profiles */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label>Permission Profiles</Label>
|
|
||||||
<div className="grid gap-3">
|
|
||||||
{permissions?.availableProfiles.map((profile) => (
|
|
||||||
<div
|
|
||||||
key={profile.id}
|
|
||||||
className={cn(
|
|
||||||
'p-4 rounded-xl border transition-colors',
|
|
||||||
permissions.activeProfile === profile.id
|
|
||||||
? 'border-brand-500/50 bg-brand-500/5'
|
|
||||||
: 'border-border/50 bg-card/50 hover:bg-accent/30'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
{profile.id === 'strict' ? (
|
|
||||||
<ShieldCheck className="w-4 h-4 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<ShieldAlert className="w-4 h-4 text-blue-500" />
|
|
||||||
)}
|
|
||||||
<span className="font-medium">{profile.name}</span>
|
|
||||||
{permissions.activeProfile === profile.id && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
Active
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mb-2">
|
|
||||||
{profile.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
<span className="text-green-500">
|
|
||||||
{profile.permissions.allow.length} allowed
|
|
||||||
</span>
|
|
||||||
<span className="text-muted-foreground/50">|</span>
|
|
||||||
<span className="text-red-500">
|
|
||||||
{profile.permissions.deny.length} denied
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={
|
|
||||||
permissions.activeProfile === profile.id
|
|
||||||
? 'secondary'
|
|
||||||
: 'default'
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
isSavingPermissions || permissions.activeProfile === profile.id
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
handleApplyProfile(
|
|
||||||
profile.id as 'strict' | 'development',
|
|
||||||
'global'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Apply Globally
|
|
||||||
</Button>
|
|
||||||
{currentProject && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={isSavingPermissions}
|
|
||||||
onClick={() =>
|
|
||||||
handleApplyProfile(
|
|
||||||
profile.id as 'strict' | 'development',
|
|
||||||
'project'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Apply to Project
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Config File Location */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label>Config File Locations</Label>
|
|
||||||
<div className="p-4 rounded-xl border border-border/50 bg-card/30 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">Global Config</p>
|
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
|
||||||
~/.cursor/cli-config.json
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleCopyConfig('development')}
|
|
||||||
>
|
|
||||||
{copiedConfig ? (
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-border/30 pt-2">
|
|
||||||
<p className="text-sm font-medium">Project Config</p>
|
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
|
||||||
<project>/.cursor/cli.json
|
|
||||||
</p>
|
|
||||||
{permissions?.hasProjectConfig && (
|
|
||||||
<Badge variant="secondary" className="mt-1 text-xs">
|
|
||||||
Project override active
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Documentation Link */}
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
Learn more about{' '}
|
|
||||||
<a
|
|
||||||
href="https://cursor.com/docs/cli/reference/permissions"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-brand-500 hover:underline"
|
|
||||||
>
|
|
||||||
Cursor CLI permissions
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user