refactor(ui): migrate settings view to React Query

- Migrate use-cursor-permissions to query and mutation hooks
- Migrate use-cursor-status to React Query
- Migrate use-skills-settings to useUpdateGlobalSettings mutation
- Migrate use-subagents-settings to mutation hooks
- Migrate use-subagents to useDiscoveredAgents query
- Migrate opencode-settings-tab to React Query hooks
- Migrate worktrees-section to query hooks
- Migrate codex/claude usage sections to query hooks
- Remove manual useState for loading/error states

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-15 16:22:04 +01:00
parent c4e0a7cc96
commit 20caa424fc
9 changed files with 289 additions and 690 deletions

View File

@@ -1,103 +1,52 @@
import { useState, useCallback } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { toast } from 'sonner';
import { useState, useCallback, useEffect } from 'react';
import { useCursorPermissionsQuery, type CursorPermissionsData } from '@/hooks/queries';
import { useApplyCursorProfile, useCopyCursorConfig } from '@/hooks/mutations';
const logger = createLogger('CursorPermissions');
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[] };
}>;
}
// Re-export for backward compatibility
export type PermissionsData = CursorPermissionsData;
/**
* 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) {
logger.error('Failed to load Cursor permissions:', error);
} finally {
setIsLoadingPermissions(false);
}
}, [projectPath]);
// React Query hooks
const permissionsQuery = useCursorPermissionsQuery(projectPath);
const applyProfileMutation = useApplyCursorProfile(projectPath);
const copyConfigMutation = useCopyCursorConfig();
// 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);
}
(profileId: 'strict' | 'development', scope: 'global' | 'project') => {
applyProfileMutation.mutate({ profileId, scope });
},
[projectPath, loadPermissions]
[applyProfileMutation]
);
// Copy example config to clipboard
const copyConfig = useCallback(async (profileId: 'strict' | 'development') => {
try {
const api = getHttpApiClient();
const result = await api.setup.getCursorExampleConfig(profileId);
const copyConfig = useCallback(
(profileId: 'strict' | 'development') => {
copyConfigMutation.mutate(profileId, {
onSuccess: () => {
setCopiedConfig(true);
setTimeout(() => setCopiedConfig(false), 2000);
},
});
},
[copyConfigMutation]
);
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');
}
}, []);
// Load permissions (refetch)
const loadPermissions = useCallback(() => {
permissionsQuery.refetch();
}, [permissionsQuery]);
return {
permissions,
isLoadingPermissions,
isSavingPermissions,
permissions: permissionsQuery.data ?? null,
isLoadingPermissions: permissionsQuery.isLoading,
isSavingPermissions: applyProfileMutation.isPending,
copiedConfig,
loadPermissions,
applyProfile,

View File

@@ -1,9 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { toast } from 'sonner';
const logger = createLogger('CursorStatus');
import { getHttpApiClient } from '@/lib/http-api-client';
import { useEffect, useMemo, useCallback } from 'react';
import { useCursorCliStatus } from '@/hooks/queries';
import { useSetupStore } from '@/store/setup-store';
export interface CursorStatus {
@@ -15,52 +11,42 @@ export interface CursorStatus {
/**
* Custom hook for managing Cursor CLI status
* Handles checking CLI installation, authentication, and refresh functionality
* Uses React Query for data fetching with automatic caching.
*/
export function useCursorStatus() {
const { setCursorCliStatus } = useSetupStore();
const { data: result, isLoading, refetch } = useCursorCliStatus();
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) {
logger.error('Failed to load Cursor settings:', error);
toast.error('Failed to load Cursor settings');
} finally {
setIsLoading(false);
}
}, [setCursorCliStatus]);
// Transform the API result into the local CursorStatus shape
const status = useMemo((): CursorStatus | null => {
if (!result) return null;
return {
installed: result.installed ?? false,
version: result.version ?? undefined,
authenticated: result.auth?.authenticated ?? false,
method: result.auth?.method,
};
}, [result]);
// Keep the global setup store in sync with query data
useEffect(() => {
loadData();
}, [loadData]);
if (status) {
setCursorCliStatus({
installed: status.installed,
version: status.version,
auth: status.authenticated
? {
authenticated: true,
method: status.method || 'unknown',
}
: undefined,
});
}
}, [status, setCursorCliStatus]);
const loadData = useCallback(() => {
refetch();
}, [refetch]);
return {
status,