Files
claudecodeui/src/components/settings/hooks/useSettingsController.ts
Haile 96463df8da Feature/backend ts support andunification of auth settings on frontend (#654)
* fix: remove project dependency from settings controller and onboarding

* fix(settings): remove onClose prop from useSettingsController args

* chore: tailwind classes order

* refactor: move provider auth status management to custom hook

* refactor: rename SessionProvider to LLMProvider

* feat(frontend): support for @ alias based imports)

* fix: replace init.sql with schema.js

* fix: refactor database initialization to use schema.js for SQL statements

* feat(server): add a real backend TypeScript build and enforce module boundaries

The backend had started to grow beyond what the frontend-only tooling setup could
support safely. We were still running server code directly from /server, linting
mainly the client, and relying on path assumptions such as "../.." that only
worked in the source layout. That created three problems:

- backend alias imports were hard to resolve consistently in the editor, ESLint,
  and the runtime
- server code had no enforced module boundary rules, so cross-module deep imports
  could bypass intended public entry points
- building the backend into a separate output directory would break repo-level
  lookups for package.json, .env, dist, and public assets because those paths
  were derived from source-only relative assumptions

This change makes the backend tooling explicit and runtime-safe.

A dedicated backend TypeScript config now lives in server/tsconfig.json, with
tsconfig.server.json reduced to a compatibility shim. This gives the language
service and backend tooling a canonical project rooted in /server while still
preserving top-level compatibility for any existing references. The backend alias
mapping now resolves relative to /server, which avoids colliding with the
frontend's "@/..." -> "src/*" mapping.

The package scripts were updated so development runs through tsx with the backend
tsconfig, build now produces a compiled backend in dist-server, and typecheck/lint
cover both client and server. A new build-server.mjs script runs TypeScript and
tsc-alias and cleans dist-server first, which prevents stale compiled files from
shadowing current source files after refactors.

To make the compiled backend behave the same as the source backend, runtime path
resolution was centralized in server/utils/runtime-paths.js. Instead of assuming
fixed relative paths from each module, server entry points now resolve the actual
app root and server root at runtime. That keeps package.json, .env, dist, public,
and default database paths stable whether code is executed from /server or from
/dist-server/server.

ESLint was expanded from a frontend-only setup into a backend-aware one. The
backend now uses import resolution tied to the backend tsconfig so aliased imports
resolve correctly in linting, import ordering matches the frontend style, and
unused/duplicate imports are surfaced consistently.

Most importantly, eslint-plugin-boundaries now enforces server module boundaries.
Files under server/modules can no longer import another module's internals
directly. Cross-module imports must go through that module's barrel file
(index.ts/index.js). boundaries/no-unknown was also enabled so alias-resolution
gaps cannot silently bypass the rule.

Together, these changes make the backend buildable, keep runtime path resolution
stable after compilation, align server tooling with the client where appropriate,
and enforce a stricter modular architecture for server code.

* fix: update package.json to include dist-server in files and remove tsconfig.server.json

* refactor: remove build-server.mjs and inline its logic into package.json scripts

* fix: update paths in package.json and bin.js to use dist-server directory

* feat(eslint): add backend shared types and enforce compile-time contract for imports

* fix(eslint): update shared types pattern

---------

Co-authored-by: Haileyesus <something@gmail.com>
2026-04-15 13:26:12 +02:00

865 lines
27 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { useTheme } from '../../../contexts/ThemeContext';
import { authenticatedFetch } from '../../../utils/api';
import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus';
import {
DEFAULT_CODE_EDITOR_SETTINGS,
DEFAULT_CURSOR_PERMISSIONS,
} from '../constants/constants';
import type {
AgentProvider,
ClaudeMcpFormState,
ClaudePermissionsState,
CodeEditorSettingsState,
CodexMcpFormState,
CodexPermissionMode,
CursorPermissionsState,
GeminiPermissionMode,
McpServer,
McpToolsResult,
McpTestResult,
NotificationPreferencesState,
ProjectSortOrder,
SettingsMainTab,
} from '../types/types';
type ThemeContextValue = {
isDarkMode: boolean;
toggleDarkMode: () => void;
};
type UseSettingsControllerArgs = {
isOpen: boolean;
initialTab: string;
};
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 NotificationPreferencesResponse = {
success?: boolean;
preferences?: NotificationPreferencesState;
};
type ActiveLoginProvider = AgentProvider | '';
const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'notifications', 'plugins'];
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 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,
});
const createDefaultNotificationPreferences = (): NotificationPreferencesState => ({
channels: {
inApp: true,
webPush: false,
},
events: {
actionRequired: true,
stop: true,
error: true,
},
});
export function useSettingsController({ isOpen, initialTab }: UseSettingsControllerArgs) {
const { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue;
const closeTimerRef = useRef<number | null>(null);
const [activeTab, setActiveTab] = useState<SettingsMainTab>(() => normalizeMainTab(initialTab));
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 [notificationPreferences, setNotificationPreferences] = useState<NotificationPreferencesState>(() => (
createDefaultNotificationPreferences()
));
const [codexPermissionMode, setCodexPermissionMode] = useState<CodexPermissionMode>('default');
const [geminiPermissionMode, setGeminiPermissionMode] = useState<GeminiPermissionMode>('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 {
providerAuthStatus,
checkProviderAuthStatus,
refreshProviderAuthStatuses,
} = useProviderAuthStatus();
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));
const savedGeminiSettings = parseJson<{ permissionMode?: GeminiPermissionMode }>(
localStorage.getItem('gemini-settings'),
{},
);
setGeminiPermissionMode(savedGeminiSettings.permissionMode || 'default');
try {
const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences');
if (notificationResponse.ok) {
const notificationData = await toResponseJson<NotificationPreferencesResponse>(notificationResponse);
if (notificationData.success && notificationData.preferences) {
setNotificationPreferences(notificationData.preferences);
} else {
setNotificationPreferences(createDefaultNotificationPreferences());
}
} else {
setNotificationPreferences(createDefaultNotificationPreferences());
}
} catch {
setNotificationPreferences(createDefaultNotificationPreferences());
}
await Promise.all([
fetchMcpServers(),
fetchCursorMcpServers(),
fetchCodexMcpServers(),
]);
} catch (error) {
console.error('Error loading settings:', error);
setClaudePermissions(createEmptyClaudePermissions());
setCursorPermissions(createEmptyCursorPermissions());
setNotificationPreferences(createDefaultNotificationPreferences());
setCodexPermissionMode('default');
setProjectSortOrder('name');
}
}, [fetchCodexMcpServers, fetchCursorMcpServers, fetchMcpServers]);
const openLoginForProvider = useCallback((provider: AgentProvider) => {
setLoginProvider(provider);
setShowLoginModal(true);
}, []);
const handleLoginComplete = useCallback((exitCode: number) => {
if (exitCode !== 0 || !loginProvider) {
return;
}
setSaveStatus('success');
void checkProviderAuthStatus(loginProvider);
}, [checkProviderAuthStatus, loginProvider]);
const saveSettings = useCallback(async () => {
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,
}));
localStorage.setItem('gemini-settings', JSON.stringify({
permissionMode: geminiPermissionMode,
lastUpdated: now,
}));
const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences', {
method: 'PUT',
body: JSON.stringify(notificationPreferences),
});
if (!notificationResponse.ok) {
throw new Error('Failed to save notification preferences');
}
setSaveStatus('success');
} catch (error) {
console.error('Error saving settings:', error);
setSaveStatus('error');
}
}, [
claudePermissions.allowedTools,
claudePermissions.disallowedTools,
claudePermissions.skipPermissions,
codexPermissionMode,
cursorPermissions.allowedCommands,
cursorPermissions.disallowedCommands,
cursorPermissions.skipPermissions,
notificationPreferences,
geminiPermissionMode,
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 refreshProviderAuthStatuses();
}, [initialTab, isOpen, loadSettings, refreshProviderAuthStatuses]);
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]);
// Auto-save permissions and sort order with debounce
const autoSaveTimerRef = useRef<number | null>(null);
const isInitialLoadRef = useRef(true);
useEffect(() => {
// Skip auto-save on initial load (settings are being loaded from localStorage)
if (isInitialLoadRef.current) {
isInitialLoadRef.current = false;
return;
}
if (autoSaveTimerRef.current !== null) {
window.clearTimeout(autoSaveTimerRef.current);
}
autoSaveTimerRef.current = window.setTimeout(() => {
saveSettings();
}, 500);
return () => {
if (autoSaveTimerRef.current !== null) {
window.clearTimeout(autoSaveTimerRef.current);
}
};
}, [saveSettings]);
// Clear save status after 2 seconds
useEffect(() => {
if (saveStatus === null) {
return;
}
const timer = window.setTimeout(() => setSaveStatus(null), 2000);
return () => window.clearTimeout(timer);
}, [saveStatus]);
// Reset initial load flag when settings dialog opens
useEffect(() => {
if (isOpen) {
isInitialLoadRef.current = true;
}
}, [isOpen]);
useEffect(() => () => {
if (closeTimerRef.current !== null) {
window.clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
}
if (autoSaveTimerRef.current !== null) {
window.clearTimeout(autoSaveTimerRef.current);
autoSaveTimerRef.current = null;
}
}, []);
return {
activeTab,
setActiveTab,
isDarkMode,
toggleDarkMode,
saveStatus,
deleteError,
projectSortOrder,
setProjectSortOrder,
codeEditorSettings,
updateCodeEditorSetting,
claudePermissions,
setClaudePermissions,
cursorPermissions,
setCursorPermissions,
notificationPreferences,
setNotificationPreferences,
codexPermissionMode,
setCodexPermissionMode,
mcpServers,
cursorMcpServers,
codexMcpServers,
mcpTestResults,
mcpServerTools,
mcpToolsLoading,
showMcpForm,
editingMcpServer,
openMcpForm,
closeMcpForm,
submitMcpForm,
handleMcpDelete,
handleMcpTest,
handleMcpToolsDiscovery,
showCodexMcpForm,
editingCodexMcpServer,
openCodexMcpForm,
closeCodexMcpForm,
submitCodexMcpForm,
handleCodexMcpDelete,
providerAuthStatus,
geminiPermissionMode,
setGeminiPermissionMode,
openLoginForProvider,
showLoginModal,
setShowLoginModal,
loginProvider,
handleLoginComplete,
};
}