mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-04 11:45:38 +08:00
Refactor Settings, FileTree, GitPanel, Shell, and CodeEditor components (#402)
This commit is contained in:
273
src/components/settings/hooks/useCredentialsSettings.ts
Normal file
273
src/components/settings/hooks/useCredentialsSettings.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import type {
|
||||
ApiKeyItem,
|
||||
ApiKeysResponse,
|
||||
CreatedApiKey,
|
||||
GithubCredentialItem,
|
||||
GithubCredentialsResponse,
|
||||
} from '../view/tabs/api-settings/types';
|
||||
import { copyTextToClipboard } from '../../../utils/clipboard';
|
||||
|
||||
type UseCredentialsSettingsArgs = {
|
||||
confirmDeleteApiKeyText: string;
|
||||
confirmDeleteGithubCredentialText: string;
|
||||
};
|
||||
|
||||
const getApiError = (payload: { error?: string } | undefined, fallback: string) => (
|
||||
payload?.error || fallback
|
||||
);
|
||||
|
||||
export function useCredentialsSettings({
|
||||
confirmDeleteApiKeyText,
|
||||
confirmDeleteGithubCredentialText,
|
||||
}: UseCredentialsSettingsArgs) {
|
||||
const [apiKeys, setApiKeys] = useState<ApiKeyItem[]>([]);
|
||||
const [githubCredentials, setGithubCredentials] = useState<GithubCredentialItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [showNewKeyForm, setShowNewKeyForm] = useState(false);
|
||||
const [newKeyName, setNewKeyName] = useState('');
|
||||
|
||||
const [showNewGithubForm, setShowNewGithubForm] = useState(false);
|
||||
const [newGithubName, setNewGithubName] = useState('');
|
||||
const [newGithubToken, setNewGithubToken] = useState('');
|
||||
const [newGithubDescription, setNewGithubDescription] = useState('');
|
||||
|
||||
const [showToken, setShowToken] = useState<Record<string, boolean>>({});
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||
const [newlyCreatedKey, setNewlyCreatedKey] = useState<CreatedApiKey | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const [apiKeysResponse, credentialsResponse] = await Promise.all([
|
||||
authenticatedFetch('/api/settings/api-keys'),
|
||||
authenticatedFetch('/api/settings/credentials?type=github_token'),
|
||||
]);
|
||||
|
||||
const [apiKeysPayload, credentialsPayload] = await Promise.all([
|
||||
apiKeysResponse.json() as Promise<ApiKeysResponse>,
|
||||
credentialsResponse.json() as Promise<GithubCredentialsResponse>,
|
||||
]);
|
||||
|
||||
setApiKeys(apiKeysPayload.apiKeys || []);
|
||||
setGithubCredentials(credentialsPayload.credentials || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createApiKey = useCallback(async () => {
|
||||
if (!newKeyName.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/settings/api-keys', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ keyName: newKeyName.trim() }),
|
||||
});
|
||||
|
||||
const payload = await response.json() as ApiKeysResponse;
|
||||
if (!response.ok || !payload.success) {
|
||||
console.error('Error creating API key:', getApiError(payload, 'Failed to create API key'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.apiKey) {
|
||||
setNewlyCreatedKey(payload.apiKey);
|
||||
}
|
||||
setNewKeyName('');
|
||||
setShowNewKeyForm(false);
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error creating API key:', error);
|
||||
}
|
||||
}, [fetchData, newKeyName]);
|
||||
|
||||
const deleteApiKey = useCallback(async (keyId: string) => {
|
||||
if (!window.confirm(confirmDeleteApiKeyText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/settings/api-keys/${keyId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json() as ApiKeysResponse;
|
||||
console.error('Error deleting API key:', getApiError(payload, 'Failed to delete API key'));
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error deleting API key:', error);
|
||||
}
|
||||
}, [confirmDeleteApiKeyText, fetchData]);
|
||||
|
||||
const toggleApiKey = useCallback(async (keyId: string, isActive: boolean) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/settings/api-keys/${keyId}/toggle`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isActive: !isActive }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json() as ApiKeysResponse;
|
||||
console.error('Error toggling API key:', getApiError(payload, 'Failed to toggle API key'));
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error toggling API key:', error);
|
||||
}
|
||||
}, [fetchData]);
|
||||
|
||||
const createGithubCredential = useCallback(async () => {
|
||||
if (!newGithubName.trim() || !newGithubToken.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/settings/credentials', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
credentialName: newGithubName.trim(),
|
||||
credentialType: 'github_token',
|
||||
credentialValue: newGithubToken,
|
||||
description: newGithubDescription.trim(),
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json() as GithubCredentialsResponse;
|
||||
if (!response.ok || !payload.success) {
|
||||
console.error('Error creating GitHub credential:', getApiError(payload, 'Failed to create GitHub credential'));
|
||||
return;
|
||||
}
|
||||
|
||||
setNewGithubName('');
|
||||
setNewGithubToken('');
|
||||
setNewGithubDescription('');
|
||||
setShowNewGithubForm(false);
|
||||
setShowToken((prev) => ({ ...prev, new: false }));
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error creating GitHub credential:', error);
|
||||
}
|
||||
}, [fetchData, newGithubDescription, newGithubName, newGithubToken]);
|
||||
|
||||
const deleteGithubCredential = useCallback(async (credentialId: string) => {
|
||||
if (!window.confirm(confirmDeleteGithubCredentialText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/settings/credentials/${credentialId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json() as GithubCredentialsResponse;
|
||||
console.error('Error deleting GitHub credential:', getApiError(payload, 'Failed to delete GitHub credential'));
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error deleting GitHub credential:', error);
|
||||
}
|
||||
}, [confirmDeleteGithubCredentialText, fetchData]);
|
||||
|
||||
const toggleGithubCredential = useCallback(async (credentialId: string, isActive: boolean) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/settings/credentials/${credentialId}/toggle`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isActive: !isActive }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json() as GithubCredentialsResponse;
|
||||
console.error('Error toggling GitHub credential:', getApiError(payload, 'Failed to toggle GitHub credential'));
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error toggling GitHub credential:', error);
|
||||
}
|
||||
}, [fetchData]);
|
||||
|
||||
const copyToClipboard = useCallback(async (text: string, id: string) => {
|
||||
try {
|
||||
await copyTextToClipboard(text);
|
||||
setCopiedKey(id);
|
||||
window.setTimeout(() => setCopiedKey(null), 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy to clipboard:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dismissNewlyCreatedKey = useCallback(() => {
|
||||
setNewlyCreatedKey(null);
|
||||
}, []);
|
||||
|
||||
const cancelNewApiKeyForm = useCallback(() => {
|
||||
setShowNewKeyForm(false);
|
||||
setNewKeyName('');
|
||||
}, []);
|
||||
|
||||
const cancelNewGithubForm = useCallback(() => {
|
||||
setShowNewGithubForm(false);
|
||||
setNewGithubName('');
|
||||
setNewGithubToken('');
|
||||
setNewGithubDescription('');
|
||||
setShowToken((prev) => ({ ...prev, new: false }));
|
||||
}, []);
|
||||
|
||||
const toggleNewGithubTokenVisibility = useCallback(() => {
|
||||
setShowToken((prev) => ({ ...prev, new: !prev.new }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return {
|
||||
apiKeys,
|
||||
githubCredentials,
|
||||
loading,
|
||||
showNewKeyForm,
|
||||
setShowNewKeyForm,
|
||||
newKeyName,
|
||||
setNewKeyName,
|
||||
showNewGithubForm,
|
||||
setShowNewGithubForm,
|
||||
newGithubName,
|
||||
setNewGithubName,
|
||||
newGithubToken,
|
||||
setNewGithubToken,
|
||||
newGithubDescription,
|
||||
setNewGithubDescription,
|
||||
showToken,
|
||||
copiedKey,
|
||||
newlyCreatedKey,
|
||||
createApiKey,
|
||||
deleteApiKey,
|
||||
toggleApiKey,
|
||||
createGithubCredential,
|
||||
deleteGithubCredential,
|
||||
toggleGithubCredential,
|
||||
copyToClipboard,
|
||||
dismissNewlyCreatedKey,
|
||||
cancelNewApiKeyForm,
|
||||
cancelNewGithubForm,
|
||||
toggleNewGithubTokenVisibility,
|
||||
};
|
||||
}
|
||||
96
src/components/settings/hooks/useGitSettings.ts
Normal file
96
src/components/settings/hooks/useGitSettings.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
|
||||
type GitConfigResponse = {
|
||||
gitName?: string;
|
||||
gitEmail?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type SaveStatus = 'success' | 'error' | null;
|
||||
|
||||
export function useGitSettings() {
|
||||
const [gitName, setGitName] = useState('');
|
||||
const [gitEmail, setGitEmail] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>(null);
|
||||
const clearStatusTimerRef = useRef<number | null>(null);
|
||||
|
||||
const clearSaveStatus = useCallback(() => {
|
||||
if (clearStatusTimerRef.current !== null) {
|
||||
window.clearTimeout(clearStatusTimerRef.current);
|
||||
clearStatusTimerRef.current = null;
|
||||
}
|
||||
setSaveStatus(null);
|
||||
}, []);
|
||||
|
||||
const loadGitConfig = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await authenticatedFetch('/api/user/git-config');
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json() as GitConfigResponse;
|
||||
setGitName(data.gitName || '');
|
||||
setGitEmail(data.gitEmail || '');
|
||||
} catch (error) {
|
||||
console.error('Error loading git config:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveGitConfig = useCallback(async () => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const response = await authenticatedFetch('/api/user/git-config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gitName, gitEmail }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSaveStatus('success');
|
||||
clearStatusTimerRef.current = window.setTimeout(() => {
|
||||
setSaveStatus(null);
|
||||
clearStatusTimerRef.current = null;
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json() as GitConfigResponse;
|
||||
console.error('Failed to save git config:', data.error);
|
||||
setSaveStatus('error');
|
||||
} catch (error) {
|
||||
console.error('Error saving git config:', error);
|
||||
setSaveStatus('error');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [gitEmail, gitName]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadGitConfig();
|
||||
}, [loadGitConfig]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (clearStatusTimerRef.current !== null) {
|
||||
window.clearTimeout(clearStatusTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
gitName,
|
||||
setGitName,
|
||||
gitEmail,
|
||||
setGitEmail,
|
||||
isLoading,
|
||||
isSaving,
|
||||
saveStatus,
|
||||
clearSaveStatus,
|
||||
saveGitConfig,
|
||||
};
|
||||
}
|
||||
841
src/components/settings/hooks/useSettingsController.ts
Normal file
841
src/components/settings/hooks/useSettingsController.ts
Normal file
@@ -0,0 +1,841 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import {
|
||||
AUTH_STATUS_ENDPOINTS,
|
||||
DEFAULT_AUTH_STATUS,
|
||||
DEFAULT_CODE_EDITOR_SETTINGS,
|
||||
DEFAULT_CURSOR_PERMISSIONS,
|
||||
} from '../constants/constants';
|
||||
import type {
|
||||
AgentProvider,
|
||||
AuthStatus,
|
||||
ClaudeMcpFormState,
|
||||
ClaudePermissionsState,
|
||||
CodeEditorSettingsState,
|
||||
CodexMcpFormState,
|
||||
CodexPermissionMode,
|
||||
CursorPermissionsState,
|
||||
McpServer,
|
||||
McpToolsResult,
|
||||
McpTestResult,
|
||||
ProjectSortOrder,
|
||||
SettingsMainTab,
|
||||
SettingsProject,
|
||||
} from '../types/types';
|
||||
|
||||
type ThemeContextValue = {
|
||||
isDarkMode: boolean;
|
||||
toggleDarkMode: () => void;
|
||||
};
|
||||
|
||||
type UseSettingsControllerArgs = {
|
||||
isOpen: boolean;
|
||||
initialTab: string;
|
||||
projects: SettingsProject[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type StatusApiResponse = {
|
||||
authenticated?: boolean;
|
||||
email?: string | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
type JsonResult = {
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type McpReadResponse = {
|
||||
success?: boolean;
|
||||
servers?: McpServer[];
|
||||
};
|
||||
|
||||
type McpCliServer = {
|
||||
name: string;
|
||||
type?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
type McpCliReadResponse = {
|
||||
success?: boolean;
|
||||
servers?: McpCliServer[];
|
||||
};
|
||||
|
||||
type McpTestResponse = {
|
||||
testResult?: McpTestResult;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type McpToolsResponse = {
|
||||
toolsResult?: McpToolsResult;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type ClaudeSettingsStorage = {
|
||||
allowedTools?: string[];
|
||||
disallowedTools?: string[];
|
||||
skipPermissions?: boolean;
|
||||
projectSortOrder?: ProjectSortOrder;
|
||||
};
|
||||
|
||||
type CursorSettingsStorage = {
|
||||
allowedCommands?: string[];
|
||||
disallowedCommands?: string[];
|
||||
skipPermissions?: boolean;
|
||||
};
|
||||
|
||||
type CodexSettingsStorage = {
|
||||
permissionMode?: CodexPermissionMode;
|
||||
};
|
||||
|
||||
type ActiveLoginProvider = AgentProvider | '';
|
||||
|
||||
const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks'];
|
||||
|
||||
const normalizeMainTab = (tab: string): SettingsMainTab => {
|
||||
// Keep backwards compatibility with older callers that still pass "tools".
|
||||
if (tab === 'tools') {
|
||||
return 'agents';
|
||||
}
|
||||
|
||||
return KNOWN_MAIN_TABS.includes(tab as SettingsMainTab) ? (tab as SettingsMainTab) : 'agents';
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: unknown): string => (
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
);
|
||||
|
||||
const parseJson = <T>(value: string | null, fallback: T): T => {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
const toCodexPermissionMode = (value: unknown): CodexPermissionMode => {
|
||||
if (value === 'acceptEdits' || value === 'bypassPermissions') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const readCodeEditorSettings = (): CodeEditorSettingsState => ({
|
||||
theme: localStorage.getItem('codeEditorTheme') === 'light' ? 'light' : 'dark',
|
||||
wordWrap: localStorage.getItem('codeEditorWordWrap') === 'true',
|
||||
showMinimap: localStorage.getItem('codeEditorShowMinimap') !== 'false',
|
||||
lineNumbers: localStorage.getItem('codeEditorLineNumbers') !== 'false',
|
||||
fontSize: localStorage.getItem('codeEditorFontSize') ?? DEFAULT_CODE_EDITOR_SETTINGS.fontSize,
|
||||
});
|
||||
|
||||
const mapCliServersToMcpServers = (servers: McpCliServer[] = []): McpServer[] => (
|
||||
servers.map((server) => ({
|
||||
id: server.name,
|
||||
name: server.name,
|
||||
type: server.type || 'stdio',
|
||||
scope: 'user',
|
||||
config: {
|
||||
command: server.command || '',
|
||||
args: server.args || [],
|
||||
env: server.env || {},
|
||||
url: server.url || '',
|
||||
headers: server.headers || {},
|
||||
timeout: 30000,
|
||||
},
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
}))
|
||||
);
|
||||
|
||||
const getDefaultProject = (projects: SettingsProject[]): SettingsProject => {
|
||||
if (projects.length > 0) {
|
||||
return projects[0];
|
||||
}
|
||||
|
||||
const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '';
|
||||
return {
|
||||
name: 'default',
|
||||
displayName: 'default',
|
||||
fullPath: cwd,
|
||||
path: cwd,
|
||||
};
|
||||
};
|
||||
|
||||
const toResponseJson = async <T>(response: Response): Promise<T> => response.json() as Promise<T>;
|
||||
|
||||
const createEmptyClaudePermissions = (): ClaudePermissionsState => ({
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
skipPermissions: false,
|
||||
});
|
||||
|
||||
const createEmptyCursorPermissions = (): CursorPermissionsState => ({
|
||||
...DEFAULT_CURSOR_PERMISSIONS,
|
||||
});
|
||||
|
||||
export function useSettingsController({ isOpen, initialTab, projects, onClose }: UseSettingsControllerArgs) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue;
|
||||
const closeTimerRef = useRef<number | null>(null);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<SettingsMainTab>(() => normalizeMainTab(initialTab));
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [projectSortOrder, setProjectSortOrder] = useState<ProjectSortOrder>('name');
|
||||
const [codeEditorSettings, setCodeEditorSettings] = useState<CodeEditorSettingsState>(() => (
|
||||
readCodeEditorSettings()
|
||||
));
|
||||
|
||||
const [claudePermissions, setClaudePermissions] = useState<ClaudePermissionsState>(() => (
|
||||
createEmptyClaudePermissions()
|
||||
));
|
||||
const [cursorPermissions, setCursorPermissions] = useState<CursorPermissionsState>(() => (
|
||||
createEmptyCursorPermissions()
|
||||
));
|
||||
const [codexPermissionMode, setCodexPermissionMode] = useState<CodexPermissionMode>('default');
|
||||
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([]);
|
||||
const [cursorMcpServers, setCursorMcpServers] = useState<McpServer[]>([]);
|
||||
const [codexMcpServers, setCodexMcpServers] = useState<McpServer[]>([]);
|
||||
const [mcpTestResults, setMcpTestResults] = useState<Record<string, McpTestResult>>({});
|
||||
const [mcpServerTools, setMcpServerTools] = useState<Record<string, McpToolsResult>>({});
|
||||
const [mcpToolsLoading, setMcpToolsLoading] = useState<Record<string, boolean>>({});
|
||||
|
||||
const [showMcpForm, setShowMcpForm] = useState(false);
|
||||
const [editingMcpServer, setEditingMcpServer] = useState<McpServer | null>(null);
|
||||
const [showCodexMcpForm, setShowCodexMcpForm] = useState(false);
|
||||
const [editingCodexMcpServer, setEditingCodexMcpServer] = useState<McpServer | null>(null);
|
||||
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [loginProvider, setLoginProvider] = useState<ActiveLoginProvider>('');
|
||||
const [selectedProject, setSelectedProject] = useState<SettingsProject | null>(null);
|
||||
|
||||
const [claudeAuthStatus, setClaudeAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [cursorAuthStatus, setCursorAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [codexAuthStatus, setCodexAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
|
||||
const setAuthStatusByProvider = useCallback((provider: AgentProvider, status: AuthStatus) => {
|
||||
if (provider === 'claude') {
|
||||
setClaudeAuthStatus(status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'cursor') {
|
||||
setCursorAuthStatus(status);
|
||||
return;
|
||||
}
|
||||
|
||||
setCodexAuthStatus(status);
|
||||
}, []);
|
||||
|
||||
const checkAuthStatus = useCallback(async (provider: AgentProvider) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(AUTH_STATUS_ENDPOINTS[provider]);
|
||||
|
||||
if (!response.ok) {
|
||||
setAuthStatusByProvider(provider, {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: 'Failed to check authentication status',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await toResponseJson<StatusApiResponse>(response);
|
||||
setAuthStatusByProvider(provider, {
|
||||
authenticated: Boolean(data.authenticated),
|
||||
email: data.email || null,
|
||||
loading: false,
|
||||
error: data.error || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error checking ${provider} auth status:`, error);
|
||||
setAuthStatusByProvider(provider, {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}, [setAuthStatusByProvider]);
|
||||
|
||||
const fetchCursorMcpServers = useCallback(async () => {
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/cursor/mcp');
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch Cursor MCP servers');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await toResponseJson<{ servers?: McpServer[] }>(response);
|
||||
setCursorMcpServers(data.servers || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Cursor MCP servers:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchCodexMcpServers = useCallback(async () => {
|
||||
try {
|
||||
const configResponse = await authenticatedFetch('/api/codex/mcp/config/read');
|
||||
|
||||
if (configResponse.ok) {
|
||||
const configData = await toResponseJson<McpReadResponse>(configResponse);
|
||||
if (configData.success && configData.servers) {
|
||||
setCodexMcpServers(configData.servers);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const cliResponse = await authenticatedFetch('/api/codex/mcp/cli/list');
|
||||
if (!cliResponse.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cliData = await toResponseJson<McpCliReadResponse>(cliResponse);
|
||||
if (!cliData.success || !cliData.servers) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCodexMcpServers(mapCliServersToMcpServers(cliData.servers));
|
||||
} catch (error) {
|
||||
console.error('Error fetching Codex MCP servers:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchMcpServers = useCallback(async () => {
|
||||
try {
|
||||
const configResponse = await authenticatedFetch('/api/mcp/config/read');
|
||||
if (configResponse.ok) {
|
||||
const configData = await toResponseJson<McpReadResponse>(configResponse);
|
||||
if (configData.success && configData.servers) {
|
||||
setMcpServers(configData.servers);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const cliResponse = await authenticatedFetch('/api/mcp/cli/list');
|
||||
if (cliResponse.ok) {
|
||||
const cliData = await toResponseJson<McpCliReadResponse>(cliResponse);
|
||||
if (cliData.success && cliData.servers) {
|
||||
setMcpServers(mapCliServersToMcpServers(cliData.servers));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackResponse = await authenticatedFetch('/api/mcp/servers?scope=user');
|
||||
if (!fallbackResponse.ok) {
|
||||
console.error('Failed to fetch MCP servers');
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackData = await toResponseJson<{ servers?: McpServer[] }>(fallbackResponse);
|
||||
setMcpServers(fallbackData.servers || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching MCP servers:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deleteMcpServer = useCallback(async (serverId: string, scope = 'user') => {
|
||||
const response = await authenticatedFetch(`/api/mcp/cli/remove/${serverId}?scope=${scope}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await toResponseJson<JsonResult>(response);
|
||||
throw new Error(error.error || 'Failed to delete server');
|
||||
}
|
||||
|
||||
const result = await toResponseJson<JsonResult>(response);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete server via Claude CLI');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveMcpServer = useCallback(
|
||||
async (serverData: ClaudeMcpFormState, editingServer: McpServer | null) => {
|
||||
const newServerScope = serverData.scope || 'user';
|
||||
|
||||
const response = await authenticatedFetch('/api/mcp/cli/add', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: serverData.name,
|
||||
type: serverData.type,
|
||||
scope: newServerScope,
|
||||
projectPath: serverData.projectPath,
|
||||
command: serverData.config.command,
|
||||
args: serverData.config.args || [],
|
||||
url: serverData.config.url,
|
||||
headers: serverData.config.headers || {},
|
||||
env: serverData.config.env || {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await toResponseJson<JsonResult>(response);
|
||||
throw new Error(error.error || 'Failed to save server');
|
||||
}
|
||||
|
||||
const result = await toResponseJson<JsonResult>(response);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save server via Claude CLI');
|
||||
}
|
||||
|
||||
if (!editingServer?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousServerScope = editingServer.scope || 'user';
|
||||
const didServerIdentityChange =
|
||||
editingServer.id !== serverData.name || previousServerScope !== newServerScope;
|
||||
|
||||
if (!didServerIdentityChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteMcpServer(editingServer.id, previousServerScope);
|
||||
} catch (error) {
|
||||
console.warn('Saved MCP server update but failed to remove the previous server entry.', {
|
||||
previousServerId: editingServer.id,
|
||||
previousServerScope,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
[deleteMcpServer],
|
||||
);
|
||||
|
||||
const submitMcpForm = useCallback(
|
||||
async (formData: ClaudeMcpFormState, editingServer: McpServer | null) => {
|
||||
if (formData.importMode === 'json') {
|
||||
const response = await authenticatedFetch('/api/mcp/cli/add-json', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
jsonConfig: formData.jsonInput,
|
||||
scope: formData.scope,
|
||||
projectPath: formData.projectPath,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await toResponseJson<JsonResult>(response);
|
||||
throw new Error(error.error || 'Failed to add server');
|
||||
}
|
||||
|
||||
const result = await toResponseJson<JsonResult>(response);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to add server via JSON');
|
||||
}
|
||||
} else {
|
||||
await saveMcpServer(formData, editingServer);
|
||||
}
|
||||
|
||||
await fetchMcpServers();
|
||||
setSaveStatus('success');
|
||||
setShowMcpForm(false);
|
||||
setEditingMcpServer(null);
|
||||
},
|
||||
[fetchMcpServers, saveMcpServer],
|
||||
);
|
||||
|
||||
const handleMcpDelete = useCallback(
|
||||
async (serverId: string, scope = 'user') => {
|
||||
if (!window.confirm('Are you sure you want to delete this MCP server?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleteError(null);
|
||||
try {
|
||||
await deleteMcpServer(serverId, scope);
|
||||
await fetchMcpServers();
|
||||
setDeleteError(null);
|
||||
setSaveStatus('success');
|
||||
} catch (error) {
|
||||
setDeleteError(getErrorMessage(error));
|
||||
setSaveStatus('error');
|
||||
}
|
||||
},
|
||||
[deleteMcpServer, fetchMcpServers],
|
||||
);
|
||||
|
||||
const testMcpServer = useCallback(async (serverId: string, scope = 'user') => {
|
||||
const response = await authenticatedFetch(`/api/mcp/servers/${serverId}/test?scope=${scope}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await toResponseJson<McpTestResponse>(response);
|
||||
throw new Error(error.error || 'Failed to test server');
|
||||
}
|
||||
|
||||
const data = await toResponseJson<McpTestResponse>(response);
|
||||
return data.testResult || { success: false, message: 'No test result returned' };
|
||||
}, []);
|
||||
|
||||
const discoverMcpTools = useCallback(async (serverId: string, scope = 'user') => {
|
||||
const response = await authenticatedFetch(`/api/mcp/servers/${serverId}/tools?scope=${scope}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await toResponseJson<McpToolsResponse>(response);
|
||||
throw new Error(error.error || 'Failed to discover tools');
|
||||
}
|
||||
|
||||
const data = await toResponseJson<McpToolsResponse>(response);
|
||||
return data.toolsResult || { success: false, tools: [], resources: [], prompts: [] };
|
||||
}, []);
|
||||
|
||||
const handleMcpTest = useCallback(
|
||||
async (serverId: string, scope = 'user') => {
|
||||
try {
|
||||
setMcpTestResults((prev) => ({
|
||||
...prev,
|
||||
[serverId]: { success: false, message: 'Testing server...', details: [], loading: true },
|
||||
}));
|
||||
|
||||
const result = await testMcpServer(serverId, scope);
|
||||
setMcpTestResults((prev) => ({ ...prev, [serverId]: result }));
|
||||
} catch (error) {
|
||||
setMcpTestResults((prev) => ({
|
||||
...prev,
|
||||
[serverId]: {
|
||||
success: false,
|
||||
message: getErrorMessage(error),
|
||||
details: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
[testMcpServer],
|
||||
);
|
||||
|
||||
const handleMcpToolsDiscovery = useCallback(
|
||||
async (serverId: string, scope = 'user') => {
|
||||
try {
|
||||
setMcpToolsLoading((prev) => ({ ...prev, [serverId]: true }));
|
||||
const result = await discoverMcpTools(serverId, scope);
|
||||
setMcpServerTools((prev) => ({ ...prev, [serverId]: result }));
|
||||
} catch {
|
||||
setMcpServerTools((prev) => ({
|
||||
...prev,
|
||||
[serverId]: { success: false, tools: [], resources: [], prompts: [] },
|
||||
}));
|
||||
} finally {
|
||||
setMcpToolsLoading((prev) => ({ ...prev, [serverId]: false }));
|
||||
}
|
||||
},
|
||||
[discoverMcpTools],
|
||||
);
|
||||
|
||||
const deleteCodexMcpServer = useCallback(async (serverId: string) => {
|
||||
const response = await authenticatedFetch(`/api/codex/mcp/cli/remove/${serverId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await toResponseJson<JsonResult>(response);
|
||||
throw new Error(error.error || 'Failed to delete server');
|
||||
}
|
||||
|
||||
const result = await toResponseJson<JsonResult>(response);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete Codex MCP server');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveCodexMcpServer = useCallback(
|
||||
async (serverData: CodexMcpFormState, editingServer: McpServer | null) => {
|
||||
const response = await authenticatedFetch('/api/codex/mcp/cli/add', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: serverData.name,
|
||||
command: serverData.config.command,
|
||||
args: serverData.config.args || [],
|
||||
env: serverData.config.env || {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await toResponseJson<JsonResult>(response);
|
||||
throw new Error(error.error || 'Failed to save server');
|
||||
}
|
||||
|
||||
const result = await toResponseJson<JsonResult>(response);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save Codex MCP server');
|
||||
}
|
||||
|
||||
if (!editingServer?.name || editingServer.name === serverData.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteCodexMcpServer(editingServer.name);
|
||||
} catch (error) {
|
||||
console.warn('Saved Codex MCP server update but failed to remove the previous server entry.', {
|
||||
previousServerName: editingServer.name,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
[deleteCodexMcpServer],
|
||||
);
|
||||
|
||||
const submitCodexMcpForm = useCallback(
|
||||
async (formData: CodexMcpFormState, editingServer: McpServer | null) => {
|
||||
await saveCodexMcpServer(formData, editingServer);
|
||||
await fetchCodexMcpServers();
|
||||
setSaveStatus('success');
|
||||
setShowCodexMcpForm(false);
|
||||
setEditingCodexMcpServer(null);
|
||||
},
|
||||
[fetchCodexMcpServers, saveCodexMcpServer],
|
||||
);
|
||||
|
||||
const handleCodexMcpDelete = useCallback(
|
||||
async (serverName: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this MCP server?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleteError(null);
|
||||
try {
|
||||
await deleteCodexMcpServer(serverName);
|
||||
await fetchCodexMcpServers();
|
||||
setDeleteError(null);
|
||||
setSaveStatus('success');
|
||||
} catch (error) {
|
||||
setDeleteError(getErrorMessage(error));
|
||||
setSaveStatus('error');
|
||||
}
|
||||
},
|
||||
[deleteCodexMcpServer, fetchCodexMcpServers],
|
||||
);
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
try {
|
||||
const savedClaudeSettings = parseJson<ClaudeSettingsStorage>(
|
||||
localStorage.getItem('claude-settings'),
|
||||
{},
|
||||
);
|
||||
setClaudePermissions({
|
||||
allowedTools: savedClaudeSettings.allowedTools || [],
|
||||
disallowedTools: savedClaudeSettings.disallowedTools || [],
|
||||
skipPermissions: Boolean(savedClaudeSettings.skipPermissions),
|
||||
});
|
||||
setProjectSortOrder(savedClaudeSettings.projectSortOrder === 'date' ? 'date' : 'name');
|
||||
|
||||
const savedCursorSettings = parseJson<CursorSettingsStorage>(
|
||||
localStorage.getItem('cursor-tools-settings'),
|
||||
{},
|
||||
);
|
||||
setCursorPermissions({
|
||||
allowedCommands: savedCursorSettings.allowedCommands || [],
|
||||
disallowedCommands: savedCursorSettings.disallowedCommands || [],
|
||||
skipPermissions: Boolean(savedCursorSettings.skipPermissions),
|
||||
});
|
||||
|
||||
const savedCodexSettings = parseJson<CodexSettingsStorage>(
|
||||
localStorage.getItem('codex-settings'),
|
||||
{},
|
||||
);
|
||||
setCodexPermissionMode(toCodexPermissionMode(savedCodexSettings.permissionMode));
|
||||
|
||||
await Promise.all([
|
||||
fetchMcpServers(),
|
||||
fetchCursorMcpServers(),
|
||||
fetchCodexMcpServers(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
setClaudePermissions(createEmptyClaudePermissions());
|
||||
setCursorPermissions(createEmptyCursorPermissions());
|
||||
setCodexPermissionMode('default');
|
||||
setProjectSortOrder('name');
|
||||
}
|
||||
}, [fetchCodexMcpServers, fetchCursorMcpServers, fetchMcpServers]);
|
||||
|
||||
const openLoginForProvider = useCallback((provider: AgentProvider) => {
|
||||
setLoginProvider(provider);
|
||||
setSelectedProject(getDefaultProject(projects));
|
||||
setShowLoginModal(true);
|
||||
}, [projects]);
|
||||
|
||||
const handleLoginComplete = useCallback((exitCode: number) => {
|
||||
if (exitCode !== 0 || !loginProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaveStatus('success');
|
||||
void checkAuthStatus(loginProvider);
|
||||
}, [checkAuthStatus, loginProvider]);
|
||||
|
||||
const saveSettings = useCallback(() => {
|
||||
setIsSaving(true);
|
||||
setSaveStatus(null);
|
||||
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
localStorage.setItem('claude-settings', JSON.stringify({
|
||||
allowedTools: claudePermissions.allowedTools,
|
||||
disallowedTools: claudePermissions.disallowedTools,
|
||||
skipPermissions: claudePermissions.skipPermissions,
|
||||
projectSortOrder,
|
||||
lastUpdated: now,
|
||||
}));
|
||||
|
||||
localStorage.setItem('cursor-tools-settings', JSON.stringify({
|
||||
allowedCommands: cursorPermissions.allowedCommands,
|
||||
disallowedCommands: cursorPermissions.disallowedCommands,
|
||||
skipPermissions: cursorPermissions.skipPermissions,
|
||||
lastUpdated: now,
|
||||
}));
|
||||
|
||||
localStorage.setItem('codex-settings', JSON.stringify({
|
||||
permissionMode: codexPermissionMode,
|
||||
lastUpdated: now,
|
||||
}));
|
||||
|
||||
setSaveStatus('success');
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
closeTimerRef.current = window.setTimeout(() => onClose(), 1000);
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
setSaveStatus('error');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [
|
||||
claudePermissions.allowedTools,
|
||||
claudePermissions.disallowedTools,
|
||||
claudePermissions.skipPermissions,
|
||||
codexPermissionMode,
|
||||
cursorPermissions.allowedCommands,
|
||||
cursorPermissions.disallowedCommands,
|
||||
cursorPermissions.skipPermissions,
|
||||
onClose,
|
||||
projectSortOrder,
|
||||
]);
|
||||
|
||||
const updateCodeEditorSetting = useCallback(
|
||||
<K extends keyof CodeEditorSettingsState>(key: K, value: CodeEditorSettingsState[K]) => {
|
||||
setCodeEditorSettings((prev) => ({ ...prev, [key]: value }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const openMcpForm = useCallback((server?: McpServer) => {
|
||||
setEditingMcpServer(server || null);
|
||||
setShowMcpForm(true);
|
||||
}, []);
|
||||
|
||||
const closeMcpForm = useCallback(() => {
|
||||
setShowMcpForm(false);
|
||||
setEditingMcpServer(null);
|
||||
}, []);
|
||||
|
||||
const openCodexMcpForm = useCallback((server?: McpServer) => {
|
||||
setEditingCodexMcpServer(server || null);
|
||||
setShowCodexMcpForm(true);
|
||||
}, []);
|
||||
|
||||
const closeCodexMcpForm = useCallback(() => {
|
||||
setShowCodexMcpForm(false);
|
||||
setEditingCodexMcpServer(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTab(normalizeMainTab(initialTab));
|
||||
void loadSettings();
|
||||
void checkAuthStatus('claude');
|
||||
void checkAuthStatus('cursor');
|
||||
void checkAuthStatus('codex');
|
||||
}, [checkAuthStatus, initialTab, isOpen, loadSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorTheme', codeEditorSettings.theme);
|
||||
localStorage.setItem('codeEditorWordWrap', String(codeEditorSettings.wordWrap));
|
||||
localStorage.setItem('codeEditorShowMinimap', String(codeEditorSettings.showMinimap));
|
||||
localStorage.setItem('codeEditorLineNumbers', String(codeEditorSettings.lineNumbers));
|
||||
localStorage.setItem('codeEditorFontSize', codeEditorSettings.fontSize);
|
||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||
}, [codeEditorSettings]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
isDarkMode,
|
||||
toggleDarkMode,
|
||||
isSaving,
|
||||
saveStatus,
|
||||
deleteError,
|
||||
projectSortOrder,
|
||||
setProjectSortOrder,
|
||||
codeEditorSettings,
|
||||
updateCodeEditorSetting,
|
||||
claudePermissions,
|
||||
setClaudePermissions,
|
||||
cursorPermissions,
|
||||
setCursorPermissions,
|
||||
codexPermissionMode,
|
||||
setCodexPermissionMode,
|
||||
mcpServers,
|
||||
cursorMcpServers,
|
||||
codexMcpServers,
|
||||
mcpTestResults,
|
||||
mcpServerTools,
|
||||
mcpToolsLoading,
|
||||
showMcpForm,
|
||||
editingMcpServer,
|
||||
openMcpForm,
|
||||
closeMcpForm,
|
||||
submitMcpForm,
|
||||
handleMcpDelete,
|
||||
handleMcpTest,
|
||||
handleMcpToolsDiscovery,
|
||||
showCodexMcpForm,
|
||||
editingCodexMcpServer,
|
||||
openCodexMcpForm,
|
||||
closeCodexMcpForm,
|
||||
submitCodexMcpForm,
|
||||
handleCodexMcpDelete,
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
openLoginForProvider,
|
||||
showLoginModal,
|
||||
setShowLoginModal,
|
||||
loginProvider,
|
||||
selectedProject,
|
||||
handleLoginComplete,
|
||||
saveSettings,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user