mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
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:
@@ -1,12 +1,10 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useClaudeUsage } from '@/hooks/queries';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { RefreshCw, AlertCircle } from 'lucide-react';
|
import { RefreshCw, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
const ERROR_NO_API = 'Claude usage API not available';
|
|
||||||
const CLAUDE_USAGE_TITLE = 'Claude Usage';
|
const CLAUDE_USAGE_TITLE = 'Claude Usage';
|
||||||
const CLAUDE_USAGE_SUBTITLE = 'Shows usage limits reported by the Claude CLI.';
|
const CLAUDE_USAGE_SUBTITLE = 'Shows usage limits reported by the Claude CLI.';
|
||||||
const CLAUDE_AUTH_WARNING = 'Authenticate Claude CLI to view usage limits.';
|
const CLAUDE_AUTH_WARNING = 'Authenticate Claude CLI to view usage limits.';
|
||||||
@@ -14,13 +12,10 @@ const CLAUDE_LOGIN_COMMAND = 'claude login';
|
|||||||
const CLAUDE_NO_USAGE_MESSAGE =
|
const CLAUDE_NO_USAGE_MESSAGE =
|
||||||
'Usage limits are not available yet. Try refreshing if this persists.';
|
'Usage limits are not available yet. Try refreshing if this persists.';
|
||||||
const UPDATED_LABEL = 'Updated';
|
const UPDATED_LABEL = 'Updated';
|
||||||
const CLAUDE_FETCH_ERROR = 'Failed to fetch usage';
|
|
||||||
const CLAUDE_REFRESH_LABEL = 'Refresh Claude usage';
|
const CLAUDE_REFRESH_LABEL = 'Refresh Claude usage';
|
||||||
const WARNING_THRESHOLD = 75;
|
const WARNING_THRESHOLD = 75;
|
||||||
const CAUTION_THRESHOLD = 50;
|
const CAUTION_THRESHOLD = 50;
|
||||||
const MAX_PERCENTAGE = 100;
|
const MAX_PERCENTAGE = 100;
|
||||||
const REFRESH_INTERVAL_MS = 60_000;
|
|
||||||
const STALE_THRESHOLD_MS = 2 * 60_000;
|
|
||||||
// Using purple/indigo for Claude branding
|
// Using purple/indigo for Claude branding
|
||||||
const USAGE_COLOR_CRITICAL = 'bg-red-500';
|
const USAGE_COLOR_CRITICAL = 'bg-red-500';
|
||||||
const USAGE_COLOR_WARNING = 'bg-amber-500';
|
const USAGE_COLOR_WARNING = 'bg-amber-500';
|
||||||
@@ -80,77 +75,31 @@ function UsageCard({
|
|||||||
|
|
||||||
export function ClaudeUsageSection() {
|
export function ClaudeUsageSection() {
|
||||||
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
|
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
|
||||||
const { claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = useAppStore();
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const canFetchUsage = !!claudeAuthStatus?.authenticated;
|
const canFetchUsage = !!claudeAuthStatus?.authenticated;
|
||||||
|
|
||||||
|
// Use React Query for data fetching with automatic polling
|
||||||
|
const {
|
||||||
|
data: claudeUsage,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
dataUpdatedAt,
|
||||||
|
refetch,
|
||||||
|
} = useClaudeUsage(canFetchUsage);
|
||||||
|
|
||||||
// If we have usage data, we can show it even if auth status is unsure
|
// If we have usage data, we can show it even if auth status is unsure
|
||||||
const hasUsage = !!claudeUsage;
|
const hasUsage = !!claudeUsage;
|
||||||
|
|
||||||
const lastUpdatedLabel = claudeUsageLastUpdated
|
const lastUpdatedLabel = useMemo(() => {
|
||||||
? new Date(claudeUsageLastUpdated).toLocaleString()
|
return dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleString() : null;
|
||||||
: null;
|
}, [dataUpdatedAt]);
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : error ? String(error) : null;
|
||||||
|
|
||||||
const showAuthWarning =
|
const showAuthWarning =
|
||||||
(!canFetchUsage && !hasUsage && !isLoading) ||
|
(!canFetchUsage && !hasUsage && !isLoading) ||
|
||||||
(error && error.includes('Authentication required'));
|
(errorMessage && errorMessage.includes('Authentication required'));
|
||||||
|
|
||||||
const isStale =
|
|
||||||
!claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > STALE_THRESHOLD_MS;
|
|
||||||
|
|
||||||
const fetchUsage = useCallback(async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api.claude) {
|
|
||||||
setError(ERROR_NO_API);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.claude.getUsage();
|
|
||||||
|
|
||||||
if ('error' in result) {
|
|
||||||
// Check for auth errors specifically
|
|
||||||
if (
|
|
||||||
result.message?.includes('Authentication required') ||
|
|
||||||
result.error?.includes('Authentication required')
|
|
||||||
) {
|
|
||||||
// We'll show the auth warning UI instead of a generic error
|
|
||||||
} else {
|
|
||||||
setError(result.message || result.error);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setClaudeUsage(result);
|
|
||||||
} catch (fetchError) {
|
|
||||||
const message = fetchError instanceof Error ? fetchError.message : CLAUDE_FETCH_ERROR;
|
|
||||||
setError(message);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [setClaudeUsage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initial fetch if authenticated and stale
|
|
||||||
// Compute staleness inside effect to avoid re-running when Date.now() changes
|
|
||||||
const isDataStale =
|
|
||||||
!claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > STALE_THRESHOLD_MS;
|
|
||||||
if (canFetchUsage && isDataStale) {
|
|
||||||
void fetchUsage();
|
|
||||||
}
|
|
||||||
}, [fetchUsage, canFetchUsage, claudeUsageLastUpdated]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canFetchUsage) return undefined;
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
void fetchUsage();
|
|
||||||
}, REFRESH_INTERVAL_MS);
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [fetchUsage, canFetchUsage]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -172,13 +121,13 @@ export function ClaudeUsageSection() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={fetchUsage}
|
onClick={() => refetch()}
|
||||||
disabled={isLoading}
|
disabled={isFetching}
|
||||||
className="ml-auto h-9 w-9 rounded-lg hover:bg-accent/50"
|
className="ml-auto h-9 w-9 rounded-lg hover:bg-accent/50"
|
||||||
data-testid="refresh-claude-usage"
|
data-testid="refresh-claude-usage"
|
||||||
title={CLAUDE_REFRESH_LABEL}
|
title={CLAUDE_REFRESH_LABEL}
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
<RefreshCw className={cn('w-4 h-4', isFetching && 'animate-spin')} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground/80 ml-12">{CLAUDE_USAGE_SUBTITLE}</p>
|
<p className="text-sm text-muted-foreground/80 ml-12">{CLAUDE_USAGE_SUBTITLE}</p>
|
||||||
@@ -194,10 +143,10 @@ export function ClaudeUsageSection() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && !showAuthWarning && (
|
{errorMessage && !showAuthWarning && (
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
||||||
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
|
||||||
<div className="text-sm text-red-400">{error}</div>
|
<div className="text-sm text-red-400">{errorMessage}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -219,7 +168,7 @@ export function ClaudeUsageSection() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasUsage && !error && !showAuthWarning && !isLoading && (
|
{!hasUsage && !errorMessage && !showAuthWarning && !isLoading && (
|
||||||
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4 text-xs text-muted-foreground">
|
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4 text-xs text-muted-foreground">
|
||||||
{CLAUDE_NO_USAGE_MESSAGE}
|
{CLAUDE_NO_USAGE_MESSAGE}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { RefreshCw, AlertCircle } from 'lucide-react';
|
import { RefreshCw, AlertCircle } from 'lucide-react';
|
||||||
import { OpenAIIcon } from '@/components/ui/provider-icon';
|
import { OpenAIIcon } from '@/components/ui/provider-icon';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
|
||||||
import {
|
import {
|
||||||
formatCodexPlanType,
|
formatCodexPlanType,
|
||||||
formatCodexResetTime,
|
formatCodexResetTime,
|
||||||
getCodexWindowLabel,
|
getCodexWindowLabel,
|
||||||
} from '@/lib/codex-usage-format';
|
} from '@/lib/codex-usage-format';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
import { useAppStore, type CodexRateLimitWindow } from '@/store/app-store';
|
import { useCodexUsage } from '@/hooks/queries';
|
||||||
|
import type { CodexRateLimitWindow } from '@/store/app-store';
|
||||||
|
|
||||||
const ERROR_NO_API = 'Codex usage API not available';
|
|
||||||
const CODEX_USAGE_TITLE = 'Codex Usage';
|
const CODEX_USAGE_TITLE = 'Codex Usage';
|
||||||
const CODEX_USAGE_SUBTITLE = 'Shows usage limits reported by the Codex CLI.';
|
const CODEX_USAGE_SUBTITLE = 'Shows usage limits reported by the Codex CLI.';
|
||||||
const CODEX_AUTH_WARNING = 'Authenticate Codex CLI to view usage limits.';
|
const CODEX_AUTH_WARNING = 'Authenticate Codex CLI to view usage limits.';
|
||||||
@@ -21,14 +18,11 @@ const CODEX_LOGIN_COMMAND = 'codex login';
|
|||||||
const CODEX_NO_USAGE_MESSAGE =
|
const CODEX_NO_USAGE_MESSAGE =
|
||||||
'Usage limits are not available yet. Try refreshing if this persists.';
|
'Usage limits are not available yet. Try refreshing if this persists.';
|
||||||
const UPDATED_LABEL = 'Updated';
|
const UPDATED_LABEL = 'Updated';
|
||||||
const CODEX_FETCH_ERROR = 'Failed to fetch usage';
|
|
||||||
const CODEX_REFRESH_LABEL = 'Refresh Codex usage';
|
const CODEX_REFRESH_LABEL = 'Refresh Codex usage';
|
||||||
const PLAN_LABEL = 'Plan';
|
const PLAN_LABEL = 'Plan';
|
||||||
const WARNING_THRESHOLD = 75;
|
const WARNING_THRESHOLD = 75;
|
||||||
const CAUTION_THRESHOLD = 50;
|
const CAUTION_THRESHOLD = 50;
|
||||||
const MAX_PERCENTAGE = 100;
|
const MAX_PERCENTAGE = 100;
|
||||||
const REFRESH_INTERVAL_MS = 60_000;
|
|
||||||
const STALE_THRESHOLD_MS = 2 * 60_000;
|
|
||||||
const USAGE_COLOR_CRITICAL = 'bg-red-500';
|
const USAGE_COLOR_CRITICAL = 'bg-red-500';
|
||||||
const USAGE_COLOR_WARNING = 'bg-amber-500';
|
const USAGE_COLOR_WARNING = 'bg-amber-500';
|
||||||
const USAGE_COLOR_OK = 'bg-emerald-500';
|
const USAGE_COLOR_OK = 'bg-emerald-500';
|
||||||
@@ -39,11 +33,12 @@ const isRateLimitWindow = (
|
|||||||
|
|
||||||
export function CodexUsageSection() {
|
export function CodexUsageSection() {
|
||||||
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
|
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
|
||||||
const { codexUsage, codexUsageLastUpdated, setCodexUsage } = useAppStore();
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const canFetchUsage = !!codexAuthStatus?.authenticated;
|
const canFetchUsage = !!codexAuthStatus?.authenticated;
|
||||||
|
|
||||||
|
// Use React Query for data fetching with automatic polling
|
||||||
|
const { data: codexUsage, isLoading, isFetching, error, refetch } = useCodexUsage(canFetchUsage);
|
||||||
|
|
||||||
const rateLimits = codexUsage?.rateLimits ?? null;
|
const rateLimits = codexUsage?.rateLimits ?? null;
|
||||||
const primary = rateLimits?.primary ?? null;
|
const primary = rateLimits?.primary ?? null;
|
||||||
const secondary = rateLimits?.secondary ?? null;
|
const secondary = rateLimits?.secondary ?? null;
|
||||||
@@ -54,46 +49,7 @@ export function CodexUsageSection() {
|
|||||||
? new Date(codexUsage.lastUpdated).toLocaleString()
|
? new Date(codexUsage.lastUpdated).toLocaleString()
|
||||||
: null;
|
: null;
|
||||||
const showAuthWarning = !canFetchUsage && !codexUsage && !isLoading;
|
const showAuthWarning = !canFetchUsage && !codexUsage && !isLoading;
|
||||||
const isStale = !codexUsageLastUpdated || Date.now() - codexUsageLastUpdated > STALE_THRESHOLD_MS;
|
const errorMessage = error instanceof Error ? error.message : error ? String(error) : null;
|
||||||
|
|
||||||
const fetchUsage = useCallback(async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api.codex) {
|
|
||||||
setError(ERROR_NO_API);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.codex.getUsage();
|
|
||||||
if ('error' in result) {
|
|
||||||
setError(result.message || result.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCodexUsage(result);
|
|
||||||
} catch (fetchError) {
|
|
||||||
const message = fetchError instanceof Error ? fetchError.message : CODEX_FETCH_ERROR;
|
|
||||||
setError(message);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [setCodexUsage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (canFetchUsage && isStale) {
|
|
||||||
void fetchUsage();
|
|
||||||
}
|
|
||||||
}, [fetchUsage, canFetchUsage, isStale]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canFetchUsage) return undefined;
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
void fetchUsage();
|
|
||||||
}, REFRESH_INTERVAL_MS);
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [fetchUsage, canFetchUsage]);
|
|
||||||
|
|
||||||
const getUsageColor = (percentage: number) => {
|
const getUsageColor = (percentage: number) => {
|
||||||
if (percentage >= WARNING_THRESHOLD) {
|
if (percentage >= WARNING_THRESHOLD) {
|
||||||
@@ -162,13 +118,13 @@ export function CodexUsageSection() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={fetchUsage}
|
onClick={() => refetch()}
|
||||||
disabled={isLoading}
|
disabled={isFetching}
|
||||||
className="ml-auto h-9 w-9 rounded-lg hover:bg-accent/50"
|
className="ml-auto h-9 w-9 rounded-lg hover:bg-accent/50"
|
||||||
data-testid="refresh-codex-usage"
|
data-testid="refresh-codex-usage"
|
||||||
title={CODEX_REFRESH_LABEL}
|
title={CODEX_REFRESH_LABEL}
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
<RefreshCw className={cn('w-4 h-4', isFetching && 'animate-spin')} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground/80 ml-12">{CODEX_USAGE_SUBTITLE}</p>
|
<p className="text-sm text-muted-foreground/80 ml-12">{CODEX_USAGE_SUBTITLE}</p>
|
||||||
@@ -182,10 +138,10 @@ export function CodexUsageSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{errorMessage && (
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
||||||
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
|
||||||
<div className="text-sm text-red-400">{error}</div>
|
<div className="text-sm text-red-400">{errorMessage}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasMetrics && (
|
{hasMetrics && (
|
||||||
@@ -210,7 +166,7 @@ export function CodexUsageSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!hasMetrics && !error && canFetchUsage && !isLoading && (
|
{!hasMetrics && !errorMessage && canFetchUsage && !isLoading && (
|
||||||
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4 text-xs text-muted-foreground">
|
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4 text-xs text-muted-foreground">
|
||||||
{CODEX_NO_USAGE_MESSAGE}
|
{CODEX_NO_USAGE_MESSAGE}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,103 +1,52 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { useCursorPermissionsQuery, type CursorPermissionsData } from '@/hooks/queries';
|
||||||
import { toast } from 'sonner';
|
import { useApplyCursorProfile, useCopyCursorConfig } from '@/hooks/mutations';
|
||||||
|
|
||||||
const logger = createLogger('CursorPermissions');
|
// Re-export for backward compatibility
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
export type PermissionsData = CursorPermissionsData;
|
||||||
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
|
* Custom hook for managing Cursor CLI permissions
|
||||||
* Handles loading permissions data, applying profiles, and copying configs
|
* Handles loading permissions data, applying profiles, and copying configs
|
||||||
*/
|
*/
|
||||||
export function useCursorPermissions(projectPath?: string) {
|
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);
|
const [copiedConfig, setCopiedConfig] = useState(false);
|
||||||
|
|
||||||
// Load permissions data
|
// React Query hooks
|
||||||
const loadPermissions = useCallback(async () => {
|
const permissionsQuery = useCursorPermissionsQuery(projectPath);
|
||||||
setIsLoadingPermissions(true);
|
const applyProfileMutation = useApplyCursorProfile(projectPath);
|
||||||
try {
|
const copyConfigMutation = useCopyCursorConfig();
|
||||||
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]);
|
|
||||||
|
|
||||||
// Apply a permission profile
|
// Apply a permission profile
|
||||||
const applyProfile = useCallback(
|
const applyProfile = useCallback(
|
||||||
async (profileId: 'strict' | 'development', scope: 'global' | 'project') => {
|
(profileId: 'strict' | 'development', scope: 'global' | 'project') => {
|
||||||
setIsSavingPermissions(true);
|
applyProfileMutation.mutate({ profileId, scope });
|
||||||
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]
|
[applyProfileMutation]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Copy example config to clipboard
|
// Copy example config to clipboard
|
||||||
const copyConfig = useCallback(async (profileId: 'strict' | 'development') => {
|
const copyConfig = useCallback(
|
||||||
try {
|
(profileId: 'strict' | 'development') => {
|
||||||
const api = getHttpApiClient();
|
copyConfigMutation.mutate(profileId, {
|
||||||
const result = await api.setup.getCursorExampleConfig(profileId);
|
onSuccess: () => {
|
||||||
|
setCopiedConfig(true);
|
||||||
|
setTimeout(() => setCopiedConfig(false), 2000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[copyConfigMutation]
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success && result.config) {
|
// Load permissions (refetch)
|
||||||
await navigator.clipboard.writeText(result.config);
|
const loadPermissions = useCallback(() => {
|
||||||
setCopiedConfig(true);
|
permissionsQuery.refetch();
|
||||||
toast.success('Config copied to clipboard');
|
}, [permissionsQuery]);
|
||||||
setTimeout(() => setCopiedConfig(false), 2000);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Failed to copy config');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permissions,
|
permissions: permissionsQuery.data ?? null,
|
||||||
isLoadingPermissions,
|
isLoadingPermissions: permissionsQuery.isLoading,
|
||||||
isSavingPermissions,
|
isSavingPermissions: applyProfileMutation.isPending,
|
||||||
copiedConfig,
|
copiedConfig,
|
||||||
loadPermissions,
|
loadPermissions,
|
||||||
applyProfile,
|
applyProfile,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useEffect, useMemo, useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { useCursorCliStatus } from '@/hooks/queries';
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
const logger = createLogger('CursorStatus');
|
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
|
|
||||||
export interface CursorStatus {
|
export interface CursorStatus {
|
||||||
@@ -15,52 +11,42 @@ export interface CursorStatus {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook for managing Cursor CLI status
|
* 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() {
|
export function useCursorStatus() {
|
||||||
const { setCursorCliStatus } = useSetupStore();
|
const { setCursorCliStatus } = useSetupStore();
|
||||||
|
const { data: result, isLoading, refetch } = useCursorCliStatus();
|
||||||
|
|
||||||
const [status, setStatus] = useState<CursorStatus | null>(null);
|
// Transform the API result into the local CursorStatus shape
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const status = useMemo((): CursorStatus | null => {
|
||||||
|
if (!result) return null;
|
||||||
const loadData = useCallback(async () => {
|
return {
|
||||||
setIsLoading(true);
|
installed: result.installed ?? false,
|
||||||
try {
|
version: result.version ?? undefined,
|
||||||
const api = getHttpApiClient();
|
authenticated: result.auth?.authenticated ?? false,
|
||||||
const statusResult = await api.setup.getCursorStatus();
|
method: result.auth?.method,
|
||||||
|
};
|
||||||
if (statusResult.success) {
|
}, [result]);
|
||||||
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]);
|
|
||||||
|
|
||||||
|
// Keep the global setup store in sync with query data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
if (status) {
|
||||||
}, [loadData]);
|
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 {
|
return {
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -5,59 +5,53 @@
|
|||||||
* configuring which sources to load Skills from (user/project).
|
* configuring which sources to load Skills from (user/project).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { useUpdateGlobalSettings } from '@/hooks/mutations';
|
||||||
|
|
||||||
export function useSkillsSettings() {
|
export function useSkillsSettings() {
|
||||||
const enabled = useAppStore((state) => state.enableSkills);
|
const enabled = useAppStore((state) => state.enableSkills);
|
||||||
const sources = useAppStore((state) => state.skillsSources);
|
const sources = useAppStore((state) => state.skillsSources);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const updateEnabled = async (newEnabled: boolean) => {
|
// React Query mutation (disable default toast)
|
||||||
setIsLoading(true);
|
const updateSettingsMutation = useUpdateGlobalSettings({ showSuccessToast: false });
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api.settings) {
|
|
||||||
throw new Error('Settings API not available');
|
|
||||||
}
|
|
||||||
await api.settings.updateGlobal({ enableSkills: newEnabled });
|
|
||||||
// Update local store after successful server update
|
|
||||||
useAppStore.setState({ enableSkills: newEnabled });
|
|
||||||
toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Failed to update skills settings');
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSources = async (newSources: Array<'user' | 'project'>) => {
|
const updateEnabled = useCallback(
|
||||||
setIsLoading(true);
|
(newEnabled: boolean) => {
|
||||||
try {
|
updateSettingsMutation.mutate(
|
||||||
const api = getElectronAPI();
|
{ enableSkills: newEnabled },
|
||||||
if (!api.settings) {
|
{
|
||||||
throw new Error('Settings API not available');
|
onSuccess: () => {
|
||||||
}
|
useAppStore.setState({ enableSkills: newEnabled });
|
||||||
await api.settings.updateGlobal({ skillsSources: newSources });
|
toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled');
|
||||||
// Update local store after successful server update
|
},
|
||||||
useAppStore.setState({ skillsSources: newSources });
|
}
|
||||||
toast.success('Skills sources updated');
|
);
|
||||||
} catch (error) {
|
},
|
||||||
toast.error('Failed to update skills sources');
|
[updateSettingsMutation]
|
||||||
console.error(error);
|
);
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
const updateSources = useCallback(
|
||||||
}
|
(newSources: Array<'user' | 'project'>) => {
|
||||||
};
|
updateSettingsMutation.mutate(
|
||||||
|
{ skillsSources: newSources },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
useAppStore.setState({ skillsSources: newSources });
|
||||||
|
toast.success('Skills sources updated');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[updateSettingsMutation]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
sources,
|
sources,
|
||||||
updateEnabled,
|
updateEnabled,
|
||||||
updateSources,
|
updateSources,
|
||||||
isLoading,
|
isLoading: updateSettingsMutation.isPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,59 +5,53 @@
|
|||||||
* configuring which sources to load Subagents from (user/project).
|
* configuring which sources to load Subagents from (user/project).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { useUpdateGlobalSettings } from '@/hooks/mutations';
|
||||||
|
|
||||||
export function useSubagentsSettings() {
|
export function useSubagentsSettings() {
|
||||||
const enabled = useAppStore((state) => state.enableSubagents);
|
const enabled = useAppStore((state) => state.enableSubagents);
|
||||||
const sources = useAppStore((state) => state.subagentsSources);
|
const sources = useAppStore((state) => state.subagentsSources);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const updateEnabled = async (newEnabled: boolean) => {
|
// React Query mutation (disable default toast)
|
||||||
setIsLoading(true);
|
const updateSettingsMutation = useUpdateGlobalSettings({ showSuccessToast: false });
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api.settings) {
|
|
||||||
throw new Error('Settings API not available');
|
|
||||||
}
|
|
||||||
await api.settings.updateGlobal({ enableSubagents: newEnabled });
|
|
||||||
// Update local store after successful server update
|
|
||||||
useAppStore.setState({ enableSubagents: newEnabled });
|
|
||||||
toast.success(newEnabled ? 'Subagents enabled' : 'Subagents disabled');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Failed to update subagents settings');
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSources = async (newSources: Array<'user' | 'project'>) => {
|
const updateEnabled = useCallback(
|
||||||
setIsLoading(true);
|
(newEnabled: boolean) => {
|
||||||
try {
|
updateSettingsMutation.mutate(
|
||||||
const api = getElectronAPI();
|
{ enableSubagents: newEnabled },
|
||||||
if (!api.settings) {
|
{
|
||||||
throw new Error('Settings API not available');
|
onSuccess: () => {
|
||||||
}
|
useAppStore.setState({ enableSubagents: newEnabled });
|
||||||
await api.settings.updateGlobal({ subagentsSources: newSources });
|
toast.success(newEnabled ? 'Subagents enabled' : 'Subagents disabled');
|
||||||
// Update local store after successful server update
|
},
|
||||||
useAppStore.setState({ subagentsSources: newSources });
|
}
|
||||||
toast.success('Subagents sources updated');
|
);
|
||||||
} catch (error) {
|
},
|
||||||
toast.error('Failed to update subagents sources');
|
[updateSettingsMutation]
|
||||||
console.error(error);
|
);
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
const updateSources = useCallback(
|
||||||
}
|
(newSources: Array<'user' | 'project'>) => {
|
||||||
};
|
updateSettingsMutation.mutate(
|
||||||
|
{ subagentsSources: newSources },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
useAppStore.setState({ subagentsSources: newSources });
|
||||||
|
toast.success('Subagents sources updated');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[updateSettingsMutation]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
sources,
|
sources,
|
||||||
updateEnabled,
|
updateEnabled,
|
||||||
updateSources,
|
updateSources,
|
||||||
isLoading,
|
isLoading: updateSettingsMutation.isPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
* Agent definitions in settings JSON are used server-side only.
|
* Agent definitions in settings JSON are used server-side only.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useMemo, useCallback } from 'react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import type { AgentDefinition } from '@automaker/types';
|
import type { AgentDefinition } from '@automaker/types';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { useDiscoveredAgents } from '@/hooks/queries';
|
||||||
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
|
|
||||||
export type SubagentScope = 'global' | 'project';
|
export type SubagentScope = 'global' | 'project';
|
||||||
export type SubagentType = 'filesystem';
|
export type SubagentType = 'filesystem';
|
||||||
@@ -35,51 +37,40 @@ interface FilesystemAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSubagents() {
|
export function useSubagents() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const currentProject = useAppStore((state) => state.currentProject);
|
const currentProject = useAppStore((state) => state.currentProject);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [subagentsWithScope, setSubagentsWithScope] = useState<SubagentWithScope[]>([]);
|
|
||||||
|
|
||||||
// Fetch filesystem agents
|
// Use React Query hook for fetching agents
|
||||||
const fetchFilesystemAgents = useCallback(async () => {
|
const {
|
||||||
setIsLoading(true);
|
data: agents = [],
|
||||||
try {
|
isLoading,
|
||||||
const api = getElectronAPI();
|
refetch,
|
||||||
if (!api.settings) {
|
} = useDiscoveredAgents(currentProject?.path, ['user', 'project']);
|
||||||
console.warn('Settings API not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await api.settings.discoverAgents(currentProject?.path, ['user', 'project']);
|
|
||||||
|
|
||||||
if (data.success && data.agents) {
|
// Transform agents to SubagentWithScope format
|
||||||
// Transform filesystem agents to SubagentWithScope format
|
const subagentsWithScope = useMemo((): SubagentWithScope[] => {
|
||||||
const agents: SubagentWithScope[] = data.agents.map(
|
return agents.map(({ name, definition, source, filePath }: FilesystemAgent) => ({
|
||||||
({ name, definition, source, filePath }: FilesystemAgent) => ({
|
name,
|
||||||
name,
|
definition,
|
||||||
definition,
|
scope: source === 'user' ? 'global' : 'project',
|
||||||
scope: source === 'user' ? 'global' : 'project',
|
type: 'filesystem' as const,
|
||||||
type: 'filesystem' as const,
|
source,
|
||||||
source,
|
filePath,
|
||||||
filePath,
|
}));
|
||||||
})
|
}, [agents]);
|
||||||
);
|
|
||||||
setSubagentsWithScope(agents);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch filesystem agents:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [currentProject?.path]);
|
|
||||||
|
|
||||||
// Fetch filesystem agents on mount and when project changes
|
// Refresh function that invalidates the query cache
|
||||||
useEffect(() => {
|
const refreshFilesystemAgents = useCallback(async () => {
|
||||||
fetchFilesystemAgents();
|
await queryClient.invalidateQueries({
|
||||||
}, [fetchFilesystemAgents]);
|
queryKey: queryKeys.settings.agents(currentProject?.path ?? ''),
|
||||||
|
});
|
||||||
|
await refetch();
|
||||||
|
}, [queryClient, currentProject?.path, refetch]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subagentsWithScope,
|
subagentsWithScope,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasProject: !!currentProject,
|
hasProject: !!currentProject,
|
||||||
refreshFilesystemAgents: fetchFilesystemAgents,
|
refreshFilesystemAgents,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,238 +1,77 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { OpencodeCliStatus, OpencodeCliStatusSkeleton } from '../cli-status/opencode-cli-status';
|
import { OpencodeCliStatus, OpencodeCliStatusSkeleton } from '../cli-status/opencode-cli-status';
|
||||||
import { OpencodeModelConfiguration } from './opencode-model-configuration';
|
import { OpencodeModelConfiguration } from './opencode-model-configuration';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { useOpencodeCliStatus, useOpencodeProviders, useOpencodeModels } from '@/hooks/queries';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
import type { CliStatus as SharedCliStatus } from '../shared/types';
|
import type { CliStatus as SharedCliStatus } from '../shared/types';
|
||||||
import type { OpencodeModelId } from '@automaker/types';
|
import type { OpencodeModelId } from '@automaker/types';
|
||||||
import type { OpencodeAuthStatus, OpenCodeProviderInfo } from '../cli-status/opencode-cli-status';
|
import type { OpencodeAuthStatus, OpenCodeProviderInfo } from '../cli-status/opencode-cli-status';
|
||||||
|
|
||||||
const logger = createLogger('OpencodeSettings');
|
|
||||||
const OPENCODE_PROVIDER_ID = 'opencode';
|
|
||||||
const OPENCODE_PROVIDER_SIGNATURE_SEPARATOR = '|';
|
|
||||||
const OPENCODE_STATIC_MODEL_PROVIDERS = new Set([OPENCODE_PROVIDER_ID]);
|
|
||||||
|
|
||||||
export function OpencodeSettingsTab() {
|
export function OpencodeSettingsTab() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
enabledOpencodeModels,
|
enabledOpencodeModels,
|
||||||
opencodeDefaultModel,
|
opencodeDefaultModel,
|
||||||
setOpencodeDefaultModel,
|
setOpencodeDefaultModel,
|
||||||
toggleOpencodeModel,
|
toggleOpencodeModel,
|
||||||
setDynamicOpencodeModels,
|
|
||||||
dynamicOpencodeModels,
|
|
||||||
enabledDynamicModelIds,
|
enabledDynamicModelIds,
|
||||||
toggleDynamicModel,
|
toggleDynamicModel,
|
||||||
cachedOpencodeProviders,
|
|
||||||
setCachedOpencodeProviders,
|
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
const [isCheckingOpencodeCli, setIsCheckingOpencodeCli] = useState(false);
|
|
||||||
const [isLoadingDynamicModels, setIsLoadingDynamicModels] = useState(false);
|
|
||||||
const [cliStatus, setCliStatus] = useState<SharedCliStatus | null>(null);
|
|
||||||
const [authStatus, setAuthStatus] = useState<OpencodeAuthStatus | null>(null);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const providerRefreshSignatureRef = useRef<string>('');
|
|
||||||
|
|
||||||
// Phase 1: Load CLI status quickly on mount
|
// React Query hooks for data fetching
|
||||||
useEffect(() => {
|
const {
|
||||||
const checkOpencodeStatus = async () => {
|
data: cliStatusData,
|
||||||
setIsCheckingOpencodeCli(true);
|
isLoading: isCheckingOpencodeCli,
|
||||||
try {
|
refetch: refetchCliStatus,
|
||||||
const api = getElectronAPI();
|
} = useOpencodeCliStatus();
|
||||||
if (api?.setup?.getOpencodeStatus) {
|
|
||||||
const result = await api.setup.getOpencodeStatus();
|
const isCliInstalled = cliStatusData?.installed ?? false;
|
||||||
setCliStatus({
|
|
||||||
success: result.success,
|
const { data: providersData = [], isFetching: isFetchingProviders } = useOpencodeProviders();
|
||||||
status: result.installed ? 'installed' : 'not_installed',
|
|
||||||
method: result.auth?.method,
|
const { data: modelsData = [], isFetching: isFetchingModels } = useOpencodeModels();
|
||||||
version: result.version,
|
|
||||||
path: result.path,
|
// Transform CLI status to the expected format
|
||||||
recommendation: result.recommendation,
|
const cliStatus = useMemo((): SharedCliStatus | null => {
|
||||||
installCommands: result.installCommands,
|
if (!cliStatusData) return null;
|
||||||
});
|
return {
|
||||||
if (result.auth) {
|
success: cliStatusData.success ?? false,
|
||||||
setAuthStatus({
|
status: cliStatusData.installed ? 'installed' : 'not_installed',
|
||||||
authenticated: result.auth.authenticated,
|
method: cliStatusData.auth?.method,
|
||||||
method: (result.auth.method as OpencodeAuthStatus['method']) || 'none',
|
version: cliStatusData.version,
|
||||||
hasApiKey: result.auth.hasApiKey,
|
path: cliStatusData.path,
|
||||||
hasEnvApiKey: result.auth.hasEnvApiKey,
|
recommendation: cliStatusData.recommendation,
|
||||||
hasOAuthToken: result.auth.hasOAuthToken,
|
installCommands: cliStatusData.installCommands,
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setCliStatus({
|
|
||||||
success: false,
|
|
||||||
status: 'not_installed',
|
|
||||||
recommendation: 'OpenCode CLI detection is only available in desktop mode.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to check OpenCode CLI status:', error);
|
|
||||||
setCliStatus({
|
|
||||||
success: false,
|
|
||||||
status: 'not_installed',
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsCheckingOpencodeCli(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
checkOpencodeStatus();
|
}, [cliStatusData]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Phase 2: Load dynamic models and providers in background (only if not cached)
|
// Transform auth status to the expected format
|
||||||
useEffect(() => {
|
const authStatus = useMemo((): OpencodeAuthStatus | null => {
|
||||||
const loadDynamicContent = async () => {
|
if (!cliStatusData?.auth) return null;
|
||||||
const api = getElectronAPI();
|
return {
|
||||||
const isInstalled = cliStatus?.success && cliStatus?.status === 'installed';
|
authenticated: cliStatusData.auth.authenticated,
|
||||||
|
method: (cliStatusData.auth.method as OpencodeAuthStatus['method']) || 'none',
|
||||||
if (!isInstalled || !api?.setup) return;
|
hasApiKey: cliStatusData.auth.hasApiKey,
|
||||||
|
hasEnvApiKey: cliStatusData.auth.hasEnvApiKey,
|
||||||
// Skip if already have cached data
|
hasOAuthToken: cliStatusData.auth.hasOAuthToken,
|
||||||
const needsProviders = cachedOpencodeProviders.length === 0;
|
|
||||||
const needsModels = dynamicOpencodeModels.length === 0;
|
|
||||||
|
|
||||||
if (!needsProviders && !needsModels) return;
|
|
||||||
|
|
||||||
setIsLoadingDynamicModels(true);
|
|
||||||
try {
|
|
||||||
// Load providers if needed
|
|
||||||
if (needsProviders && api.setup.getOpencodeProviders) {
|
|
||||||
const providersResult = await api.setup.getOpencodeProviders();
|
|
||||||
if (providersResult.success && providersResult.providers) {
|
|
||||||
setCachedOpencodeProviders(providersResult.providers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load models if needed
|
|
||||||
if (needsModels && api.setup.getOpencodeModels) {
|
|
||||||
const modelsResult = await api.setup.getOpencodeModels();
|
|
||||||
if (modelsResult.success && modelsResult.models) {
|
|
||||||
setDynamicOpencodeModels(modelsResult.models);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to load dynamic content:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingDynamicModels(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
loadDynamicContent();
|
}, [cliStatusData]);
|
||||||
}, [cliStatus?.success, cliStatus?.status]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const refreshModelsForNewProviders = async () => {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
const isInstalled = cliStatus?.success && cliStatus?.status === 'installed';
|
|
||||||
|
|
||||||
if (!isInstalled || !api?.setup?.refreshOpencodeModels) return;
|
|
||||||
if (isLoadingDynamicModels) return;
|
|
||||||
|
|
||||||
const authenticatedProviders = cachedOpencodeProviders
|
|
||||||
.filter((provider) => provider.authenticated)
|
|
||||||
.map((provider) => provider.id)
|
|
||||||
.filter((providerId) => !OPENCODE_STATIC_MODEL_PROVIDERS.has(providerId));
|
|
||||||
|
|
||||||
if (authenticatedProviders.length === 0) {
|
|
||||||
providerRefreshSignatureRef.current = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dynamicProviderIds = new Set(
|
|
||||||
dynamicOpencodeModels.map((model) => model.provider).filter(Boolean)
|
|
||||||
);
|
|
||||||
const missingProviders = authenticatedProviders.filter(
|
|
||||||
(providerId) => !dynamicProviderIds.has(providerId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (missingProviders.length === 0) {
|
|
||||||
providerRefreshSignatureRef.current = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signature = [...missingProviders].sort().join(OPENCODE_PROVIDER_SIGNATURE_SEPARATOR);
|
|
||||||
if (providerRefreshSignatureRef.current === signature) return;
|
|
||||||
providerRefreshSignatureRef.current = signature;
|
|
||||||
|
|
||||||
setIsLoadingDynamicModels(true);
|
|
||||||
try {
|
|
||||||
const modelsResult = await api.setup.refreshOpencodeModels();
|
|
||||||
if (modelsResult.success && modelsResult.models) {
|
|
||||||
setDynamicOpencodeModels(modelsResult.models);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to refresh OpenCode models for new providers:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingDynamicModels(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
refreshModelsForNewProviders();
|
|
||||||
}, [
|
|
||||||
cachedOpencodeProviders,
|
|
||||||
dynamicOpencodeModels,
|
|
||||||
cliStatus?.success,
|
|
||||||
cliStatus?.status,
|
|
||||||
isLoadingDynamicModels,
|
|
||||||
setDynamicOpencodeModels,
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
// Refresh all opencode-related queries
|
||||||
const handleRefreshOpencodeCli = useCallback(async () => {
|
const handleRefreshOpencodeCli = useCallback(async () => {
|
||||||
setIsCheckingOpencodeCli(true);
|
await Promise.all([
|
||||||
setIsLoadingDynamicModels(true);
|
queryClient.invalidateQueries({ queryKey: queryKeys.cli.opencode() }),
|
||||||
try {
|
queryClient.invalidateQueries({ queryKey: queryKeys.models.opencodeProviders() }),
|
||||||
const api = getElectronAPI();
|
queryClient.invalidateQueries({ queryKey: queryKeys.models.opencode() }),
|
||||||
if (api?.setup?.getOpencodeStatus) {
|
]);
|
||||||
const result = await api.setup.getOpencodeStatus();
|
await refetchCliStatus();
|
||||||
setCliStatus({
|
toast.success('OpenCode CLI refreshed');
|
||||||
success: result.success,
|
}, [queryClient, refetchCliStatus]);
|
||||||
status: result.installed ? 'installed' : 'not_installed',
|
|
||||||
method: result.auth?.method,
|
|
||||||
version: result.version,
|
|
||||||
path: result.path,
|
|
||||||
recommendation: result.recommendation,
|
|
||||||
installCommands: result.installCommands,
|
|
||||||
});
|
|
||||||
if (result.auth) {
|
|
||||||
setAuthStatus({
|
|
||||||
authenticated: result.auth.authenticated,
|
|
||||||
method: (result.auth.method as OpencodeAuthStatus['method']) || 'none',
|
|
||||||
hasApiKey: result.auth.hasApiKey,
|
|
||||||
hasEnvApiKey: result.auth.hasEnvApiKey,
|
|
||||||
hasOAuthToken: result.auth.hasOAuthToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.installed) {
|
|
||||||
// Refresh providers
|
|
||||||
if (api?.setup?.getOpencodeProviders) {
|
|
||||||
const providersResult = await api.setup.getOpencodeProviders();
|
|
||||||
if (providersResult.success && providersResult.providers) {
|
|
||||||
setCachedOpencodeProviders(providersResult.providers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh dynamic models
|
|
||||||
if (api?.setup?.refreshOpencodeModels) {
|
|
||||||
const modelsResult = await api.setup.refreshOpencodeModels();
|
|
||||||
if (modelsResult.success && modelsResult.models) {
|
|
||||||
setDynamicOpencodeModels(modelsResult.models);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('OpenCode CLI refreshed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to refresh OpenCode CLI status:', error);
|
|
||||||
toast.error('Failed to refresh OpenCode CLI status');
|
|
||||||
} finally {
|
|
||||||
setIsCheckingOpencodeCli(false);
|
|
||||||
setIsLoadingDynamicModels(false);
|
|
||||||
}
|
|
||||||
}, [setDynamicOpencodeModels, setCachedOpencodeProviders]);
|
|
||||||
|
|
||||||
const handleDefaultModelChange = useCallback(
|
const handleDefaultModelChange = useCallback(
|
||||||
(model: OpencodeModelId) => {
|
(model: OpencodeModelId) => {
|
||||||
@@ -240,7 +79,7 @@ export function OpencodeSettingsTab() {
|
|||||||
try {
|
try {
|
||||||
setOpencodeDefaultModel(model);
|
setOpencodeDefaultModel(model);
|
||||||
toast.success('Default model updated');
|
toast.success('Default model updated');
|
||||||
} catch (error) {
|
} catch {
|
||||||
toast.error('Failed to update default model');
|
toast.error('Failed to update default model');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@@ -254,7 +93,7 @@ export function OpencodeSettingsTab() {
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
toggleOpencodeModel(model, enabled);
|
toggleOpencodeModel(model, enabled);
|
||||||
} catch (error) {
|
} catch {
|
||||||
toast.error('Failed to update models');
|
toast.error('Failed to update models');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@@ -268,7 +107,7 @@ export function OpencodeSettingsTab() {
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
toggleDynamicModel(modelId, enabled);
|
toggleDynamicModel(modelId, enabled);
|
||||||
} catch (error) {
|
} catch {
|
||||||
toast.error('Failed to update dynamic model');
|
toast.error('Failed to update dynamic model');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@@ -286,14 +125,14 @@ export function OpencodeSettingsTab() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCliInstalled = cliStatus?.success && cliStatus?.status === 'installed';
|
const isLoadingDynamicModels = isFetchingProviders || isFetchingModels;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<OpencodeCliStatus
|
<OpencodeCliStatus
|
||||||
status={cliStatus}
|
status={cliStatus}
|
||||||
authStatus={authStatus}
|
authStatus={authStatus}
|
||||||
providers={cachedOpencodeProviders as OpenCodeProviderInfo[]}
|
providers={providersData as OpenCodeProviderInfo[]}
|
||||||
isChecking={isCheckingOpencodeCli}
|
isChecking={isCheckingOpencodeCli}
|
||||||
onRefresh={handleRefreshOpencodeCli}
|
onRefresh={handleRefreshOpencodeCli}
|
||||||
/>
|
/>
|
||||||
@@ -306,8 +145,8 @@ export function OpencodeSettingsTab() {
|
|||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
onDefaultModelChange={handleDefaultModelChange}
|
onDefaultModelChange={handleDefaultModelChange}
|
||||||
onModelToggle={handleModelToggle}
|
onModelToggle={handleModelToggle}
|
||||||
providers={cachedOpencodeProviders as OpenCodeProviderInfo[]}
|
providers={providersData as OpenCodeProviderInfo[]}
|
||||||
dynamicModels={dynamicOpencodeModels}
|
dynamicModels={modelsData}
|
||||||
enabledDynamicModelIds={enabledDynamicModelIds}
|
enabledDynamicModelIds={enabledDynamicModelIds}
|
||||||
onDynamicModelToggle={handleDynamicModelToggle}
|
onDynamicModelToggle={handleDynamicModelToggle}
|
||||||
isLoadingDynamicModels={isLoadingDynamicModels}
|
isLoadingDynamicModels={isLoadingDynamicModels}
|
||||||
|
|||||||
@@ -14,24 +14,16 @@ import {
|
|||||||
PanelBottomClose,
|
PanelBottomClose,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { apiGet, apiPut, apiDelete } from '@/lib/api-fetch';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
|
import { useWorktreeInitScript } from '@/hooks/queries';
|
||||||
|
import { useSetInitScript, useDeleteInitScript } from '@/hooks/mutations';
|
||||||
|
|
||||||
interface WorktreesSectionProps {
|
interface WorktreesSectionProps {
|
||||||
useWorktrees: boolean;
|
useWorktrees: boolean;
|
||||||
onUseWorktreesChange: (value: boolean) => void;
|
onUseWorktreesChange: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitScriptResponse {
|
|
||||||
success: boolean;
|
|
||||||
exists: boolean;
|
|
||||||
content: string;
|
|
||||||
path: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: WorktreesSectionProps) {
|
export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: WorktreesSectionProps) {
|
||||||
const currentProject = useAppStore((s) => s.currentProject);
|
const currentProject = useAppStore((s) => s.currentProject);
|
||||||
const getShowInitScriptIndicator = useAppStore((s) => s.getShowInitScriptIndicator);
|
const getShowInitScriptIndicator = useAppStore((s) => s.getShowInitScriptIndicator);
|
||||||
@@ -40,12 +32,20 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
|
|||||||
const setDefaultDeleteBranch = useAppStore((s) => s.setDefaultDeleteBranch);
|
const setDefaultDeleteBranch = useAppStore((s) => s.setDefaultDeleteBranch);
|
||||||
const getAutoDismissInitScriptIndicator = useAppStore((s) => s.getAutoDismissInitScriptIndicator);
|
const getAutoDismissInitScriptIndicator = useAppStore((s) => s.getAutoDismissInitScriptIndicator);
|
||||||
const setAutoDismissInitScriptIndicator = useAppStore((s) => s.setAutoDismissInitScriptIndicator);
|
const setAutoDismissInitScriptIndicator = useAppStore((s) => s.setAutoDismissInitScriptIndicator);
|
||||||
|
|
||||||
|
// Local state for script content editing
|
||||||
const [scriptContent, setScriptContent] = useState('');
|
const [scriptContent, setScriptContent] = useState('');
|
||||||
const [originalContent, setOriginalContent] = useState('');
|
const [originalContent, setOriginalContent] = useState('');
|
||||||
const [scriptExists, setScriptExists] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
// React Query hooks for init script
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const { data: initScriptData, isLoading } = useWorktreeInitScript(currentProject?.path);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const setInitScript = useSetInitScript(currentProject?.path ?? '');
|
||||||
|
const deleteInitScript = useDeleteInitScript(currentProject?.path ?? '');
|
||||||
|
|
||||||
|
// Derived state
|
||||||
|
const scriptExists = initScriptData?.exists ?? false;
|
||||||
|
const isSaving = setInitScript.isPending;
|
||||||
|
const isDeleting = deleteInitScript.isPending;
|
||||||
|
|
||||||
// Get the current show indicator setting
|
// Get the current show indicator setting
|
||||||
const showIndicator = currentProject?.path
|
const showIndicator = currentProject?.path
|
||||||
@@ -65,102 +65,43 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
|
|||||||
// Check if there are unsaved changes
|
// Check if there are unsaved changes
|
||||||
const hasChanges = scriptContent !== originalContent;
|
const hasChanges = scriptContent !== originalContent;
|
||||||
|
|
||||||
// Load init script content when project changes
|
// Sync query data to local state when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProject?.path) {
|
if (initScriptData) {
|
||||||
|
const content = initScriptData.content || '';
|
||||||
|
setScriptContent(content);
|
||||||
|
setOriginalContent(content);
|
||||||
|
} else if (!currentProject?.path) {
|
||||||
setScriptContent('');
|
setScriptContent('');
|
||||||
setOriginalContent('');
|
setOriginalContent('');
|
||||||
setScriptExists(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}, [initScriptData, currentProject?.path]);
|
||||||
|
|
||||||
const loadInitScript = async () => {
|
// Save script using mutation
|
||||||
setIsLoading(true);
|
const handleSave = useCallback(() => {
|
||||||
try {
|
|
||||||
const response = await apiGet<InitScriptResponse>(
|
|
||||||
`/api/worktree/init-script?projectPath=${encodeURIComponent(currentProject.path)}`
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
const content = response.content || '';
|
|
||||||
setScriptContent(content);
|
|
||||||
setOriginalContent(content);
|
|
||||||
setScriptExists(response.exists);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load init script:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadInitScript();
|
|
||||||
}, [currentProject?.path]);
|
|
||||||
|
|
||||||
// Save script
|
|
||||||
const handleSave = useCallback(async () => {
|
|
||||||
if (!currentProject?.path) return;
|
if (!currentProject?.path) return;
|
||||||
|
setInitScript.mutate(scriptContent, {
|
||||||
setIsSaving(true);
|
onSuccess: () => {
|
||||||
try {
|
|
||||||
const response = await apiPut<{ success: boolean; error?: string }>(
|
|
||||||
'/api/worktree/init-script',
|
|
||||||
{
|
|
||||||
projectPath: currentProject.path,
|
|
||||||
content: scriptContent,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
setOriginalContent(scriptContent);
|
setOriginalContent(scriptContent);
|
||||||
setScriptExists(true);
|
},
|
||||||
toast.success('Init script saved');
|
});
|
||||||
} else {
|
}, [currentProject?.path, scriptContent, setInitScript]);
|
||||||
toast.error('Failed to save init script', {
|
|
||||||
description: response.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save init script:', error);
|
|
||||||
toast.error('Failed to save init script');
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
}, [currentProject?.path, scriptContent]);
|
|
||||||
|
|
||||||
// Reset to original content
|
// Reset to original content
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setScriptContent(originalContent);
|
setScriptContent(originalContent);
|
||||||
}, [originalContent]);
|
}, [originalContent]);
|
||||||
|
|
||||||
// Delete script
|
// Delete script using mutation
|
||||||
const handleDelete = useCallback(async () => {
|
const handleDelete = useCallback(() => {
|
||||||
if (!currentProject?.path) return;
|
if (!currentProject?.path) return;
|
||||||
|
deleteInitScript.mutate(undefined, {
|
||||||
setIsDeleting(true);
|
onSuccess: () => {
|
||||||
try {
|
|
||||||
const response = await apiDelete<{ success: boolean; error?: string }>(
|
|
||||||
'/api/worktree/init-script',
|
|
||||||
{
|
|
||||||
body: { projectPath: currentProject.path },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
setScriptContent('');
|
setScriptContent('');
|
||||||
setOriginalContent('');
|
setOriginalContent('');
|
||||||
setScriptExists(false);
|
},
|
||||||
toast.success('Init script deleted');
|
});
|
||||||
} else {
|
}, [currentProject?.path, deleteInitScript]);
|
||||||
toast.error('Failed to delete init script', {
|
|
||||||
description: response.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete init script:', error);
|
|
||||||
toast.error('Failed to delete init script');
|
|
||||||
} finally {
|
|
||||||
setIsDeleting(false);
|
|
||||||
}
|
|
||||||
}, [currentProject?.path]);
|
|
||||||
|
|
||||||
// Handle content change (no auto-save)
|
// Handle content change (no auto-save)
|
||||||
const handleContentChange = useCallback((value: string) => {
|
const handleContentChange = useCallback((value: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user