diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx
deleted file mode 100644
index 9517257..0000000
--- a/src/components/Settings.jsx
+++ /dev/null
@@ -1,1977 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Button } from './ui/button';
-import { Input } from './ui/input';
-import { Badge } from './ui/badge';
-import { X, Plus, Settings as SettingsIcon, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen, LogIn, Key, GitBranch, Check } from 'lucide-react';
-import { useTheme } from '../contexts/ThemeContext';
-import { useTranslation } from 'react-i18next';
-import CredentialsSettings from './CredentialsSettings';
-import GitSettings from './GitSettings';
-import TasksSettings from './TasksSettings';
-import LoginModal from './LoginModal';
-import { authenticatedFetch } from '../utils/api';
-
-// New settings components
-import AgentListItem from './settings/AgentListItem';
-import AccountContent from './settings/AccountContent';
-import PermissionsContent from './settings/PermissionsContent';
-import McpServersContent from './settings/McpServersContent';
-import LanguageSelector from './LanguageSelector';
-
-function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
- const { isDarkMode, toggleDarkMode } = useTheme();
- const { t } = useTranslation('settings');
- const [allowedTools, setAllowedTools] = useState([]);
- const [disallowedTools, setDisallowedTools] = useState([]);
- const [newAllowedTool, setNewAllowedTool] = useState('');
- const [newDisallowedTool, setNewDisallowedTool] = useState('');
- const [skipPermissions, setSkipPermissions] = useState(false);
- const [isSaving, setIsSaving] = useState(false);
- const [saveStatus, setSaveStatus] = useState(null);
- const [projectSortOrder, setProjectSortOrder] = useState('name');
-
- const [mcpServers, setMcpServers] = useState([]);
- const [showMcpForm, setShowMcpForm] = useState(false);
- const [editingMcpServer, setEditingMcpServer] = useState(null);
- const [mcpFormData, setMcpFormData] = useState({
- name: '',
- type: 'stdio',
- scope: 'user',
- projectPath: '', // For local scope
- config: {
- command: '',
- args: [],
- env: {},
- url: '',
- headers: {},
- timeout: 30000
- },
- jsonInput: '', // For JSON import
- importMode: 'form' // 'form' or 'json'
- });
- const [mcpLoading, setMcpLoading] = useState(false);
- const [mcpTestResults, setMcpTestResults] = useState({});
- const [mcpServerTools, setMcpServerTools] = useState({});
- const [mcpToolsLoading, setMcpToolsLoading] = useState({});
- const [activeTab, setActiveTab] = useState(initialTab);
- const [jsonValidationError, setJsonValidationError] = useState('');
- const [selectedAgent, setSelectedAgent] = useState('claude'); // 'claude', 'cursor', or 'codex'
- const [selectedCategory, setSelectedCategory] = useState('account'); // 'account', 'permissions', or 'mcp'
-
- // Code Editor settings
- const [codeEditorTheme, setCodeEditorTheme] = useState(() =>
- localStorage.getItem('codeEditorTheme') || 'dark'
- );
- const [codeEditorWordWrap, setCodeEditorWordWrap] = useState(() =>
- localStorage.getItem('codeEditorWordWrap') === 'true'
- );
- const [codeEditorShowMinimap, setCodeEditorShowMinimap] = useState(() =>
- localStorage.getItem('codeEditorShowMinimap') !== 'false' // Default true
- );
- const [codeEditorLineNumbers, setCodeEditorLineNumbers] = useState(() =>
- localStorage.getItem('codeEditorLineNumbers') !== 'false' // Default true
- );
- const [codeEditorFontSize, setCodeEditorFontSize] = useState(() =>
- localStorage.getItem('codeEditorFontSize') || '14'
- );
-
- // Cursor-specific states
- const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
- const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]);
- const [cursorSkipPermissions, setCursorSkipPermissions] = useState(false);
- const [newCursorCommand, setNewCursorCommand] = useState('');
- const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState('');
- const [cursorMcpServers, setCursorMcpServers] = useState([]);
-
- // Codex-specific states
- const [codexMcpServers, setCodexMcpServers] = useState([]);
- const [codexPermissionMode, setCodexPermissionMode] = useState('default');
- const [showCodexMcpForm, setShowCodexMcpForm] = useState(false);
- const [codexMcpFormData, setCodexMcpFormData] = useState({
- name: '',
- type: 'stdio',
- config: {
- command: '',
- args: [],
- env: {}
- }
- });
- const [editingCodexMcpServer, setEditingCodexMcpServer] = useState(null);
- const [codexMcpLoading, setCodexMcpLoading] = useState(false);
-
- const [showLoginModal, setShowLoginModal] = useState(false);
- const [loginProvider, setLoginProvider] = useState('');
- const [selectedProject, setSelectedProject] = useState(null);
-
- const [claudeAuthStatus, setClaudeAuthStatus] = useState({
- authenticated: false,
- email: null,
- loading: true,
- error: null
- });
- const [cursorAuthStatus, setCursorAuthStatus] = useState({
- authenticated: false,
- email: null,
- loading: true,
- error: null
- });
- const [codexAuthStatus, setCodexAuthStatus] = useState({
- authenticated: false,
- email: null,
- loading: true,
- error: null
- });
-
- // Common tool patterns for Claude
- const commonTools = [
- 'Bash(git log:*)',
- 'Bash(git diff:*)',
- 'Bash(git status:*)',
- 'Write',
- 'Read',
- 'Edit',
- 'Glob',
- 'Grep',
- 'MultiEdit',
- 'Task',
- 'TodoWrite',
- 'TodoRead',
- 'WebFetch',
- 'WebSearch'
- ];
-
- // Common shell commands for Cursor
- const commonCursorCommands = [
- 'Shell(ls)',
- 'Shell(mkdir)',
- 'Shell(cd)',
- 'Shell(cat)',
- 'Shell(echo)',
- 'Shell(git status)',
- 'Shell(git diff)',
- 'Shell(git log)',
- 'Shell(npm install)',
- 'Shell(npm run)',
- 'Shell(python)',
- 'Shell(node)'
- ];
-
- // Fetch Cursor MCP servers
- const fetchCursorMcpServers = async () => {
- try {
- const response = await authenticatedFetch('/api/cursor/mcp');
-
- if (response.ok) {
- const data = await response.json();
- setCursorMcpServers(data.servers || []);
- } else {
- console.error('Failed to fetch Cursor MCP servers');
- }
- } catch (error) {
- console.error('Error fetching Cursor MCP servers:', error);
- }
- };
-
- const fetchCodexMcpServers = async () => {
- try {
- const configResponse = await authenticatedFetch('/api/codex/mcp/config/read');
-
- if (configResponse.ok) {
- const configData = await configResponse.json();
- if (configData.success && configData.servers) {
- setCodexMcpServers(configData.servers);
- return;
- }
- }
-
- const cliResponse = await authenticatedFetch('/api/codex/mcp/cli/list');
-
- if (cliResponse.ok) {
- const cliData = await cliResponse.json();
- if (cliData.success && cliData.servers) {
- const servers = cliData.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 || {}
- }
- }));
- setCodexMcpServers(servers);
- }
- }
- } catch (error) {
- console.error('Error fetching Codex MCP servers:', error);
- }
- };
-
- // MCP API functions
- const fetchMcpServers = async () => {
- try {
- // Try to read directly from config files for complete details
- const configResponse = await authenticatedFetch('/api/mcp/config/read');
-
- if (configResponse.ok) {
- const configData = await configResponse.json();
- if (configData.success && configData.servers) {
- setMcpServers(configData.servers);
- return;
- }
- }
-
- // Fallback to Claude CLI
- const cliResponse = await authenticatedFetch('/api/mcp/cli/list');
-
- if (cliResponse.ok) {
- const cliData = await cliResponse.json();
- if (cliData.success && cliData.servers) {
- // Convert CLI format to our format
- const servers = cliData.servers.map(server => ({
- id: server.name,
- name: server.name,
- type: server.type,
- 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()
- }));
- setMcpServers(servers);
- return;
- }
- }
-
- // Final fallback to direct config reading
- const response = await authenticatedFetch('/api/mcp/servers?scope=user');
-
- if (response.ok) {
- const data = await response.json();
- setMcpServers(data.servers || []);
- } else {
- console.error('Failed to fetch MCP servers');
- }
- } catch (error) {
- console.error('Error fetching MCP servers:', error);
- }
- };
-
- const saveMcpServer = async (serverData) => {
- try {
- if (editingMcpServer) {
- // For editing, remove old server and add new one
- await deleteMcpServer(editingMcpServer.id, 'user');
- }
-
- // Use Claude CLI to add the server
- const response = await authenticatedFetch('/api/mcp/cli/add', {
- method: 'POST',
- body: JSON.stringify({
- name: serverData.name,
- type: serverData.type,
- scope: serverData.scope,
- 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 result = await response.json();
- if (result.success) {
- await fetchMcpServers(); // Refresh the list
- return true;
- } else {
- throw new Error(result.error || 'Failed to save server via Claude CLI');
- }
- } else {
- const error = await response.json();
- throw new Error(error.error || 'Failed to save server');
- }
- } catch (error) {
- console.error('Error saving MCP server:', error);
- throw error;
- }
- };
-
- const deleteMcpServer = async (serverId, scope = 'user') => {
- try {
- // Use Claude CLI to remove the server with proper scope
- const response = await authenticatedFetch(`/api/mcp/cli/remove/${serverId}?scope=${scope}`, {
- method: 'DELETE'
- });
-
- if (response.ok) {
- const result = await response.json();
- if (result.success) {
- await fetchMcpServers(); // Refresh the list
- return true;
- } else {
- throw new Error(result.error || 'Failed to delete server via Claude CLI');
- }
- } else {
- const error = await response.json();
- throw new Error(error.error || 'Failed to delete server');
- }
- } catch (error) {
- console.error('Error deleting MCP server:', error);
- throw error;
- }
- };
-
- const testMcpServer = async (serverId, scope = 'user') => {
- try {
- const response = await authenticatedFetch(`/api/mcp/servers/${serverId}/test?scope=${scope}`, {
- method: 'POST'
- });
-
- if (response.ok) {
- const data = await response.json();
- return data.testResult;
- } else {
- const error = await response.json();
- throw new Error(error.error || 'Failed to test server');
- }
- } catch (error) {
- console.error('Error testing MCP server:', error);
- throw error;
- }
- };
-
-
- const discoverMcpTools = async (serverId, scope = 'user') => {
- try {
- const response = await authenticatedFetch(`/api/mcp/servers/${serverId}/tools?scope=${scope}`, {
- method: 'POST'
- });
-
- if (response.ok) {
- const data = await response.json();
- return data.toolsResult;
- } else {
- const error = await response.json();
- throw new Error(error.error || 'Failed to discover tools');
- }
- } catch (error) {
- console.error('Error discovering MCP tools:', error);
- throw error;
- }
- };
-
- const saveCodexMcpServer = async (serverData) => {
- try {
- if (editingCodexMcpServer) {
- await deleteCodexMcpServer(editingCodexMcpServer.id);
- }
-
- 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 result = await response.json();
- if (result.success) {
- await fetchCodexMcpServers();
- return true;
- } else {
- throw new Error(result.error || 'Failed to save Codex MCP server');
- }
- } else {
- const error = await response.json();
- throw new Error(error.error || 'Failed to save server');
- }
- } catch (error) {
- console.error('Error saving Codex MCP server:', error);
- throw error;
- }
- };
-
- const deleteCodexMcpServer = async (serverId) => {
- try {
- const response = await authenticatedFetch(`/api/codex/mcp/cli/remove/${serverId}`, {
- method: 'DELETE'
- });
-
- if (response.ok) {
- const result = await response.json();
- if (result.success) {
- await fetchCodexMcpServers();
- return true;
- } else {
- throw new Error(result.error || 'Failed to delete Codex MCP server');
- }
- } else {
- const error = await response.json();
- throw new Error(error.error || 'Failed to delete server');
- }
- } catch (error) {
- console.error('Error deleting Codex MCP server:', error);
- throw error;
- }
- };
-
- const resetCodexMcpForm = () => {
- setCodexMcpFormData({
- name: '',
- type: 'stdio',
- config: {
- command: '',
- args: [],
- env: {}
- }
- });
- setEditingCodexMcpServer(null);
- setShowCodexMcpForm(false);
- };
-
- const openCodexMcpForm = (server = null) => {
- if (server) {
- setEditingCodexMcpServer(server);
- setCodexMcpFormData({
- name: server.name,
- type: server.type || 'stdio',
- config: {
- command: server.config?.command || '',
- args: server.config?.args || [],
- env: server.config?.env || {}
- }
- });
- } else {
- resetCodexMcpForm();
- }
- setShowCodexMcpForm(true);
- };
-
- const handleCodexMcpSubmit = async (e) => {
- e.preventDefault();
- setCodexMcpLoading(true);
-
- try {
- if (editingCodexMcpServer) {
- // Delete old server first, then add new one
- await deleteCodexMcpServer(editingCodexMcpServer.name);
- }
- await saveCodexMcpServer(codexMcpFormData);
- resetCodexMcpForm();
- setSaveStatus('success');
- } catch (error) {
- alert(`Error: ${error.message}`);
- setSaveStatus('error');
- } finally {
- setCodexMcpLoading(false);
- }
- };
-
- const handleCodexMcpDelete = async (serverName) => {
- if (confirm('Are you sure you want to delete this MCP server?')) {
- try {
- await deleteCodexMcpServer(serverName);
- setSaveStatus('success');
- } catch (error) {
- alert(`Error: ${error.message}`);
- setSaveStatus('error');
- }
- }
- };
-
- useEffect(() => {
- if (isOpen) {
- loadSettings();
- checkClaudeAuthStatus();
- checkCursorAuthStatus();
- checkCodexAuthStatus();
- setActiveTab(initialTab);
- }
- }, [isOpen, initialTab]);
-
- // Persist code editor settings to localStorage
- useEffect(() => {
- localStorage.setItem('codeEditorTheme', codeEditorTheme);
- window.dispatchEvent(new Event('codeEditorSettingsChanged'));
- }, [codeEditorTheme]);
-
- useEffect(() => {
- localStorage.setItem('codeEditorWordWrap', codeEditorWordWrap.toString());
- window.dispatchEvent(new Event('codeEditorSettingsChanged'));
- }, [codeEditorWordWrap]);
-
- useEffect(() => {
- localStorage.setItem('codeEditorShowMinimap', codeEditorShowMinimap.toString());
- window.dispatchEvent(new Event('codeEditorSettingsChanged'));
- }, [codeEditorShowMinimap]);
-
- useEffect(() => {
- localStorage.setItem('codeEditorLineNumbers', codeEditorLineNumbers.toString());
- window.dispatchEvent(new Event('codeEditorSettingsChanged'));
- }, [codeEditorLineNumbers]);
-
- useEffect(() => {
- localStorage.setItem('codeEditorFontSize', codeEditorFontSize);
- window.dispatchEvent(new Event('codeEditorSettingsChanged'));
- }, [codeEditorFontSize]);
-
- const loadSettings = async () => {
- try {
-
- // Load Claude settings from localStorage
- const savedSettings = localStorage.getItem('claude-settings');
-
- if (savedSettings) {
- const settings = JSON.parse(savedSettings);
- setAllowedTools(settings.allowedTools || []);
- setDisallowedTools(settings.disallowedTools || []);
- setSkipPermissions(settings.skipPermissions || false);
- setProjectSortOrder(settings.projectSortOrder || 'name');
- } else {
- // Set defaults
- setAllowedTools([]);
- setDisallowedTools([]);
- setSkipPermissions(false);
- setProjectSortOrder('name');
- }
-
- // Load Cursor settings from localStorage
- const savedCursorSettings = localStorage.getItem('cursor-tools-settings');
-
- if (savedCursorSettings) {
- const cursorSettings = JSON.parse(savedCursorSettings);
- setCursorAllowedCommands(cursorSettings.allowedCommands || []);
- setCursorDisallowedCommands(cursorSettings.disallowedCommands || []);
- setCursorSkipPermissions(cursorSettings.skipPermissions || false);
- } else {
- // Set Cursor defaults
- setCursorAllowedCommands([]);
- setCursorDisallowedCommands([]);
- setCursorSkipPermissions(false);
- }
-
- // Load Codex settings from localStorage
- const savedCodexSettings = localStorage.getItem('codex-settings');
-
- if (savedCodexSettings) {
- const codexSettings = JSON.parse(savedCodexSettings);
- setCodexPermissionMode(codexSettings.permissionMode || 'default');
- } else {
- setCodexPermissionMode('default');
- }
-
- // Load MCP servers from API
- await fetchMcpServers();
-
- // Load Cursor MCP servers
- await fetchCursorMcpServers();
-
- // Load Codex MCP servers
- await fetchCodexMcpServers();
- } catch (error) {
- console.error('Error loading tool settings:', error);
- setAllowedTools([]);
- setDisallowedTools([]);
- setSkipPermissions(false);
- setProjectSortOrder('name');
- }
- };
-
- const checkClaudeAuthStatus = async () => {
- try {
- const response = await authenticatedFetch('/api/cli/claude/status');
-
- if (response.ok) {
- const data = await response.json();
- setClaudeAuthStatus({
- authenticated: data.authenticated,
- email: data.email,
- loading: false,
- error: data.error || null
- });
- } else {
- setClaudeAuthStatus({
- authenticated: false,
- email: null,
- loading: false,
- error: 'Failed to check authentication status'
- });
- }
- } catch (error) {
- console.error('Error checking Claude auth status:', error);
- setClaudeAuthStatus({
- authenticated: false,
- email: null,
- loading: false,
- error: error.message
- });
- }
- };
-
- const checkCursorAuthStatus = async () => {
- try {
- const response = await authenticatedFetch('/api/cli/cursor/status');
-
- if (response.ok) {
- const data = await response.json();
- setCursorAuthStatus({
- authenticated: data.authenticated,
- email: data.email,
- loading: false,
- error: data.error || null
- });
- } else {
- setCursorAuthStatus({
- authenticated: false,
- email: null,
- loading: false,
- error: 'Failed to check authentication status'
- });
- }
- } catch (error) {
- console.error('Error checking Cursor auth status:', error);
- setCursorAuthStatus({
- authenticated: false,
- email: null,
- loading: false,
- error: error.message
- });
- }
- };
-
- const checkCodexAuthStatus = async () => {
- try {
- const response = await authenticatedFetch('/api/cli/codex/status');
-
- if (response.ok) {
- const data = await response.json();
- setCodexAuthStatus({
- authenticated: data.authenticated,
- email: data.email,
- loading: false,
- error: data.error || null
- });
- } else {
- setCodexAuthStatus({
- authenticated: false,
- email: null,
- loading: false,
- error: 'Failed to check authentication status'
- });
- }
- } catch (error) {
- console.error('Error checking Codex auth status:', error);
- setCodexAuthStatus({
- authenticated: false,
- email: null,
- loading: false,
- error: error.message
- });
- }
- };
-
- const handleClaudeLogin = () => {
- setLoginProvider('claude');
- setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
- setShowLoginModal(true);
- };
-
- const handleCursorLogin = () => {
- setLoginProvider('cursor');
- setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
- setShowLoginModal(true);
- };
-
- const handleCodexLogin = () => {
- setLoginProvider('codex');
- setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
- setShowLoginModal(true);
- };
-
- const handleLoginComplete = (exitCode) => {
- if (exitCode === 0) {
- setSaveStatus('success');
-
- if (loginProvider === 'claude') {
- checkClaudeAuthStatus();
- } else if (loginProvider === 'cursor') {
- checkCursorAuthStatus();
- } else if (loginProvider === 'codex') {
- checkCodexAuthStatus();
- }
- }
- };
-
- const saveSettings = () => {
- setIsSaving(true);
- setSaveStatus(null);
-
- try {
- // Save Claude settings
- const claudeSettings = {
- allowedTools,
- disallowedTools,
- skipPermissions,
- projectSortOrder,
- lastUpdated: new Date().toISOString()
- };
-
- // Save Cursor settings
- const cursorSettings = {
- allowedCommands: cursorAllowedCommands,
- disallowedCommands: cursorDisallowedCommands,
- skipPermissions: cursorSkipPermissions,
- lastUpdated: new Date().toISOString()
- };
-
- // Save Codex settings
- const codexSettings = {
- permissionMode: codexPermissionMode,
- lastUpdated: new Date().toISOString()
- };
-
- // Save to localStorage
- localStorage.setItem('claude-settings', JSON.stringify(claudeSettings));
- localStorage.setItem('cursor-tools-settings', JSON.stringify(cursorSettings));
- localStorage.setItem('codex-settings', JSON.stringify(codexSettings));
-
- setSaveStatus('success');
-
- setTimeout(() => {
- onClose();
- }, 1000);
- } catch (error) {
- console.error('Error saving tool settings:', error);
- setSaveStatus('error');
- } finally {
- setIsSaving(false);
- }
- };
-
- const addAllowedTool = (tool) => {
- if (tool && !allowedTools.includes(tool)) {
- setAllowedTools([...allowedTools, tool]);
- setNewAllowedTool('');
- }
- };
-
- const removeAllowedTool = (tool) => {
- setAllowedTools(allowedTools.filter(t => t !== tool));
- };
-
- const addDisallowedTool = (tool) => {
- if (tool && !disallowedTools.includes(tool)) {
- setDisallowedTools([...disallowedTools, tool]);
- setNewDisallowedTool('');
- }
- };
-
- const removeDisallowedTool = (tool) => {
- setDisallowedTools(disallowedTools.filter(t => t !== tool));
- };
-
- // MCP form handling functions
- const resetMcpForm = () => {
- setMcpFormData({
- name: '',
- type: 'stdio',
- scope: 'user', // Default to user scope
- projectPath: '',
- config: {
- command: '',
- args: [],
- env: {},
- url: '',
- headers: {},
- timeout: 30000
- },
- jsonInput: '',
- importMode: 'form'
- });
- setEditingMcpServer(null);
- setShowMcpForm(false);
- setJsonValidationError('');
- };
-
- const openMcpForm = (server = null) => {
- if (server) {
- setEditingMcpServer(server);
- setMcpFormData({
- name: server.name,
- type: server.type,
- scope: server.scope,
- projectPath: server.projectPath || '',
- config: { ...server.config },
- raw: server.raw, // Store raw config for display
- importMode: 'form', // Always use form mode when editing
- jsonInput: ''
- });
- } else {
- resetMcpForm();
- }
- setShowMcpForm(true);
- };
-
- const handleMcpSubmit = async (e) => {
- e.preventDefault();
-
- setMcpLoading(true);
-
- try {
- if (mcpFormData.importMode === 'json') {
- // Use JSON import endpoint
- const response = await authenticatedFetch('/api/mcp/cli/add-json', {
- method: 'POST',
- body: JSON.stringify({
- name: mcpFormData.name,
- jsonConfig: mcpFormData.jsonInput,
- scope: mcpFormData.scope,
- projectPath: mcpFormData.projectPath
- })
- });
-
- if (response.ok) {
- const result = await response.json();
- if (result.success) {
- await fetchMcpServers(); // Refresh the list
- resetMcpForm();
- setSaveStatus('success');
- } else {
- throw new Error(result.error || 'Failed to add server via JSON');
- }
- } else {
- const error = await response.json();
- throw new Error(error.error || 'Failed to add server');
- }
- } else {
- // Use regular form-based save
- await saveMcpServer(mcpFormData);
- resetMcpForm();
- setSaveStatus('success');
- }
- } catch (error) {
- alert(`Error: ${error.message}`);
- setSaveStatus('error');
- } finally {
- setMcpLoading(false);
- }
- };
-
- const handleMcpDelete = async (serverId, scope) => {
- if (confirm('Are you sure you want to delete this MCP server?')) {
- try {
- await deleteMcpServer(serverId, scope);
- setSaveStatus('success');
- } catch (error) {
- alert(`Error: ${error.message}`);
- setSaveStatus('error');
- }
- }
- };
-
- const handleMcpTest = async (serverId, scope) => {
- try {
- setMcpTestResults({ ...mcpTestResults, [serverId]: { loading: true } });
- const result = await testMcpServer(serverId, scope);
- setMcpTestResults({ ...mcpTestResults, [serverId]: result });
- } catch (error) {
- setMcpTestResults({
- ...mcpTestResults,
- [serverId]: {
- success: false,
- message: error.message,
- details: []
- }
- });
- }
- };
-
- const handleMcpToolsDiscovery = async (serverId, scope) => {
- try {
- setMcpToolsLoading({ ...mcpToolsLoading, [serverId]: true });
- const result = await discoverMcpTools(serverId, scope);
- setMcpServerTools({ ...mcpServerTools, [serverId]: result });
- } catch (error) {
- setMcpServerTools({
- ...mcpServerTools,
- [serverId]: {
- success: false,
- tools: [],
- resources: [],
- prompts: []
- }
- });
- } finally {
- setMcpToolsLoading({ ...mcpToolsLoading, [serverId]: false });
- }
- };
-
- const updateMcpConfig = (key, value) => {
- setMcpFormData(prev => ({
- ...prev,
- config: {
- ...prev.config,
- [key]: value
- }
- }));
- };
-
-
- const getTransportIcon = (type) => {
- switch (type) {
- case 'stdio': return ;
- case 'sse': return ;
- case 'http': return ;
- default: return ;
- }
- };
-
- if (!isOpen) return null;
-
- return (
-
-
-
-
-
-
- {t('title')}
-
-
-
-
-
-
- {/* Tab Navigation */}
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Appearance Tab */}
- {activeTab === 'appearance' && (
-
- {activeTab === 'appearance' && (
-
- {/* Theme Settings */}
-
-
-
-
-
- {t('appearanceSettings.darkMode.label')}
-
-
- {t('appearanceSettings.darkMode.description')}
-
-
-
-
-
-
-
- {/* Language Selector */}
-
-
-
-
- {/* Project Sorting */}
-
-
-
-
-
- {t('appearanceSettings.projectSorting.label')}
-
-
- {t('appearanceSettings.projectSorting.description')}
-
-
-
-
-
-
-
- {/* Code Editor Settings */}
-
-
{t('appearanceSettings.codeEditor.title')}
-
- {/* Editor Theme */}
-
-
-
-
- {t('appearanceSettings.codeEditor.theme.label')}
-
-
- {t('appearanceSettings.codeEditor.theme.description')}
-
-
-
-
-
-
- {/* Word Wrap */}
-
-
-
-
- {t('appearanceSettings.codeEditor.wordWrap.label')}
-
-
- {t('appearanceSettings.codeEditor.wordWrap.description')}
-
-
-
-
-
-
- {/* Show Minimap */}
-
-
-
-
- {t('appearanceSettings.codeEditor.showMinimap.label')}
-
-
- {t('appearanceSettings.codeEditor.showMinimap.description')}
-
-
-
-
-
-
- {/* Show Line Numbers */}
-
-
-
-
- {t('appearanceSettings.codeEditor.lineNumbers.label')}
-
-
- {t('appearanceSettings.codeEditor.lineNumbers.description')}
-
-
-
-
-
-
- {/* Font Size */}
-
-
-
-
- {t('appearanceSettings.codeEditor.fontSize.label')}
-
-
- {t('appearanceSettings.codeEditor.fontSize.description')}
-
-
-
-
-
-
-
-)}
-
-
- )}
-
- {/* Git Tab */}
- {activeTab === 'git' &&
}
-
- {/* Agents Tab */}
- {activeTab === 'agents' && (
-
- {/* Mobile: Horizontal Agent Tabs */}
-
-
-
setSelectedAgent('claude')}
- isMobile={true}
- />
- setSelectedAgent('cursor')}
- isMobile={true}
- />
- setSelectedAgent('codex')}
- isMobile={true}
- />
-
-
-
- {/* Desktop: Sidebar - Agent List */}
-
-
-
setSelectedAgent('claude')}
- />
- setSelectedAgent('cursor')}
- />
- setSelectedAgent('codex')}
- />
-
-
-
- {/* Main Panel */}
-
- {/* Category Tabs */}
-
-
-
-
-
-
-
-
- {/* Category Content */}
-
- {/* Account Category */}
- {selectedCategory === 'account' && (
-
- )}
-
- {/* Permissions Category */}
- {selectedCategory === 'permissions' && selectedAgent === 'claude' && (
-
- )}
-
- {selectedCategory === 'permissions' && selectedAgent === 'cursor' && (
-
- )}
-
- {selectedCategory === 'permissions' && selectedAgent === 'codex' && (
-
- )}
-
- {/* MCP Servers Category */}
- {selectedCategory === 'mcp' && selectedAgent === 'claude' && (
-
openMcpForm()}
- onEdit={(server) => openMcpForm(server)}
- onDelete={(serverId, scope) => handleMcpDelete(serverId, scope)}
- onTest={(serverId, scope) => handleMcpTest(serverId, scope)}
- onDiscoverTools={(serverId, scope) => handleMcpToolsDiscovery(serverId, scope)}
- testResults={mcpTestResults}
- serverTools={mcpServerTools}
- toolsLoading={mcpToolsLoading}
- />
- )}
-
- {selectedCategory === 'mcp' && selectedAgent === 'cursor' && (
- {/* TODO: Add cursor MCP form */}}
- onEdit={(server) => {/* TODO: Edit cursor MCP form */}}
- onDelete={(serverId) => {/* TODO: Delete cursor MCP */}}
- />
- )}
-
- {selectedCategory === 'mcp' && selectedAgent === 'codex' && (
- openCodexMcpForm()}
- onEdit={(server) => openCodexMcpForm(server)}
- onDelete={(serverId) => handleCodexMcpDelete(serverId)}
- />
- )}
-
-
-
- )}
-
- {/* MCP Server Form Modal */}
- {showMcpForm && (
-
-
-
-
- {editingMcpServer ? t('mcpForm.title.edit') : t('mcpForm.title.add')}
-
-
-
-
-
-
-
- )}
-
- {/* Codex MCP Server Form Modal */}
- {showCodexMcpForm && (
-
-
-
-
- {editingCodexMcpServer ? t('mcpForm.title.edit') : t('mcpForm.title.add')}
-
-
-
-
-
-
-
- )}
-
- {/* Tasks Tab */}
- {activeTab === 'tasks' && (
-
-
-
- )}
-
- {/* API & Tokens Tab */}
- {activeTab === 'api' && (
-
-
-
- )}
-
-
-
-
-
- {saveStatus === 'success' && (
-
-
- {t('saveStatus.success')}
-
- )}
- {saveStatus === 'error' && (
-
-
- {t('saveStatus.error')}
-
- )}
-
-
-
-
-
-
-
-
- {/* Login Modal */}
-
setShowLoginModal(false)}
- provider={loginProvider}
- project={selectedProject}
- onComplete={handleLoginComplete}
- isAuthenticated={
- loginProvider === 'claude' ? claudeAuthStatus.authenticated :
- loginProvider === 'cursor' ? cursorAuthStatus.authenticated :
- loginProvider === 'codex' ? codexAuthStatus.authenticated :
- false
- }
- />
-
- );
-}
-
-export default Settings;
diff --git a/src/components/modals/VersionUpgradeModal.tsx b/src/components/modals/VersionUpgradeModal.tsx
deleted file mode 100644
index 4d09dcd..0000000
--- a/src/components/modals/VersionUpgradeModal.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-import { useCallback, useState } from "react";
-import { useTranslation } from "react-i18next";
-import { authenticatedFetch } from "../../utils/api";
-import { ReleaseInfo } from "../../types/sharedTypes";
-
-interface VersionUpgradeModalProps {
- isOpen: boolean;
- onClose: () => void;
- releaseInfo: ReleaseInfo | null;
- currentVersion: string;
- latestVersion: string | null;
-}
-
-export default function VersionUpgradeModal({
- isOpen,
- onClose,
- releaseInfo,
- currentVersion,
- latestVersion
-}: VersionUpgradeModalProps) {
- const { t } = useTranslation('common');
- const [isUpdating, setIsUpdating] = useState(false);
- const [updateOutput, setUpdateOutput] = useState('');
- const [updateError, setUpdateError] = useState('');
-
- const handleUpdateNow = useCallback(async () => {
- setIsUpdating(true);
- setUpdateOutput('Starting update...\n');
- setUpdateError('');
-
- try {
- // Call the backend API to run the update command
- const response = await authenticatedFetch('/api/system/update', {
- method: 'POST',
- });
-
- const data = await response.json();
-
- if (response.ok) {
- setUpdateOutput(prev => prev + data.output + '\n');
- setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n');
- setUpdateOutput(prev => prev + 'Please restart the server to apply changes.\n');
- } else {
- setUpdateError(data.error || 'Update failed');
- setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n');
- }
- } catch (error: any) {
- setUpdateError(error.message);
- setUpdateOutput(prev => prev + '\n❌ Update failed: ' + error.message + '\n');
- } finally {
- setIsUpdating(false);
- }
- }, []);
-
- if (!isOpen) return null;
-
- return (
-
- {/* Backdrop */}
-
-
- {/* Modal */}
-
- {/* Header */}
-
-
-
-
-
{t('versionUpdate.title')}
-
- {releaseInfo?.title || t('versionUpdate.newVersionReady')}
-
-
-
-
-
-
- {/* Version Info */}
-
-
- {t('versionUpdate.currentVersion')}
- {currentVersion}
-
-
- {t('versionUpdate.latestVersion')}
- {latestVersion}
-
-
-
- {/* Changelog */}
- {releaseInfo?.body && (
-
-
-
-
- {cleanChangelog(releaseInfo.body)}
-
-
-
- )}
-
- {/* Update Output */}
- {(updateOutput || updateError) && (
-
-
{t('versionUpdate.updateProgress')}
-
- {updateError && (
-
- {updateError}
-
- )}
-
- )}
-
- {/* Upgrade Instructions */}
- {!isUpdating && !updateOutput && (
-
-
{t('versionUpdate.manualUpgrade')}
-
-
- git checkout main && git pull && npm install
-
-
-
- {t('versionUpdate.manualUpgradeHint')}
-
-
- )}
-
- {/* Actions */}
-
-
- {!updateOutput && (
- <>
-
-
- >
- )}
-
-
-
- );
-};
-
-// Clean up changelog by removing GitHub-specific metadata
-const cleanChangelog = (body: string) => {
- if (!body) return '';
-
- return body
- // Remove full commit hashes (40 character hex strings)
- .replace(/\b[0-9a-f]{40}\b/gi, '')
- // Remove short commit hashes (7-10 character hex strings at start of line or after dash/space)
- .replace(/(?:^|\s|-)([0-9a-f]{7,10})\b/gi, '')
- // Remove "Full Changelog" links
- .replace(/\*\*Full Changelog\*\*:.*$/gim, '')
- // Remove compare links (e.g., https://github.com/.../compare/v1.0.0...v1.0.1)
- .replace(/https?:\/\/github\.com\/[^\/]+\/[^\/]+\/compare\/[^\s)]+/gi, '')
- // Clean up multiple consecutive empty lines
- .replace(/\n\s*\n\s*\n/g, '\n\n')
- // Trim whitespace
- .trim();
-};
diff --git a/src/components/settings/McpServersContent.jsx b/src/components/settings/McpServersContent.jsx
deleted file mode 100644
index 257f58a..0000000
--- a/src/components/settings/McpServersContent.jsx
+++ /dev/null
@@ -1,319 +0,0 @@
-import { useState } from 'react';
-import { Button } from '../ui/button';
-import { Input } from '../ui/input';
-import { Badge } from '../ui/badge';
-import { Server, Plus, Edit3, Trash2, Terminal, Globe, Zap, X } from 'lucide-react';
-import { useTranslation } from 'react-i18next';
-
-const getTransportIcon = (type) => {
- switch (type) {
- case 'stdio': return ;
- case 'sse': return ;
- case 'http': return ;
- default: return ;
- }
-};
-
-// Claude MCP Servers
-function ClaudeMcpServers({
- servers,
- onAdd,
- onEdit,
- onDelete,
- onTest,
- onDiscoverTools,
- testResults,
- serverTools,
- toolsLoading,
-}) {
- const { t } = useTranslation('settings');
- return (
-
-
-
-
- {t('mcpServers.title')}
-
-
-
- {t('mcpServers.description.claude')}
-
-
-
-
-
-
-
- {servers.map(server => (
-
-
-
-
- {getTransportIcon(server.type)}
- {server.name}
-
- {server.type}
-
-
- {server.scope === 'local' ? t('mcpServers.scope.local') : server.scope === 'user' ? t('mcpServers.scope.user') : server.scope}
-
-
-
-
- {server.type === 'stdio' && server.config?.command && (
-
{t('mcpServers.config.command')}: {server.config.command}
- )}
- {(server.type === 'sse' || server.type === 'http') && server.config?.url && (
-
{t('mcpServers.config.url')}: {server.config.url}
- )}
- {server.config?.args && server.config.args.length > 0 && (
-
{t('mcpServers.config.args')}: {server.config.args.join(' ')}
- )}
-
-
- {/* Test Results */}
- {testResults?.[server.id] && (
-
-
{testResults[server.id].message}
-
- )}
-
- {/* Tools Discovery Results */}
- {serverTools?.[server.id] && serverTools[server.id].tools?.length > 0 && (
-
-
{t('mcpServers.tools.title')} {t('mcpServers.tools.count', { count: serverTools[server.id].tools.length })}
-
- {serverTools[server.id].tools.slice(0, 5).map((tool, i) => (
- {tool.name}
- ))}
- {serverTools[server.id].tools.length > 5 && (
- {t('mcpServers.tools.more', { count: serverTools[server.id].tools.length - 5 })}
- )}
-
-
- )}
-
-
-
-
-
-
-
-
- ))}
- {servers.length === 0 && (
-
- {t('mcpServers.empty')}
-
- )}
-
-
- );
-}
-
-// Cursor MCP Servers
-function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
- const { t } = useTranslation('settings');
- return (
-
-
-
-
- {t('mcpServers.title')}
-
-
-
- {t('mcpServers.description.cursor')}
-
-
-
-
-
-
-
- {servers.map(server => (
-
-
-
-
-
- {server.name}
- stdio
-
-
- {server.config?.command && (
-
{t('mcpServers.config.command')}: {server.config.command}
- )}
-
-
-
-
-
-
-
-
- ))}
- {servers.length === 0 && (
-
- {t('mcpServers.empty')}
-
- )}
-
-
- );
-}
-
-// Codex MCP Servers
-function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
- const { t } = useTranslation('settings');
- return (
-
-
-
-
- {t('mcpServers.title')}
-
-
-
- {t('mcpServers.description.codex')}
-
-
-
-
-
-
-
- {servers.map(server => (
-
-
-
-
-
- {server.name}
- stdio
-
-
-
- {server.config?.command && (
-
{t('mcpServers.config.command')}: {server.config.command}
- )}
- {server.config?.args && server.config.args.length > 0 && (
-
{t('mcpServers.config.args')}: {server.config.args.join(' ')}
- )}
- {server.config?.env && Object.keys(server.config.env).length > 0 && (
-
{t('mcpServers.config.environment')}: {Object.entries(server.config.env).map(([k, v]) => `${k}=${v}`).join(', ')}
- )}
-
-
-
-
-
-
-
-
-
- ))}
- {servers.length === 0 && (
-
- {t('mcpServers.empty')}
-
- )}
-
-
- {/* Help Section */}
-
-
{t('mcpServers.help.title')}
-
- {t('mcpServers.help.description')}
-
-
-
- );
-}
-
-// Main component
-export default function McpServersContent({ agent, ...props }) {
- if (agent === 'claude') {
- return ;
- }
- if (agent === 'cursor') {
- return ;
- }
- if (agent === 'codex') {
- return ;
- }
- return null;
-}
diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx
new file mode 100644
index 0000000..fb6735d
--- /dev/null
+++ b/src/components/settings/Settings.tsx
@@ -0,0 +1,251 @@
+import { Settings as SettingsIcon, X } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import CredentialsSettings from '../CredentialsSettings';
+import GitSettings from '../GitSettings';
+import LoginModal from '../LoginModal';
+import TasksSettings from '../TasksSettings';
+import { Button } from '../ui/button';
+import ClaudeMcpFormModal from './view/modals/ClaudeMcpFormModal';
+import CodexMcpFormModal from './view/modals/CodexMcpFormModal';
+import SettingsMainTabs from './view/SettingsMainTabs';
+import AgentsSettingsTab from './view/tabs/agents-settings/AgentsSettingsTab';
+import AppearanceSettingsTab from './view/tabs/AppearanceSettingsTab';
+import { useSettingsController } from './hooks/useSettingsController';
+import type { AgentProvider, SettingsProject, SettingsProps } from './types/types';
+
+type LoginModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ provider: AgentProvider | '';
+ project: SettingsProject | null;
+ onComplete: (exitCode: number) => void;
+ isAuthenticated: boolean;
+};
+
+const LoginModalComponent = LoginModal as unknown as (props: LoginModalProps) => JSX.Element;
+
+function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: SettingsProps) {
+ const { t } = useTranslation('settings');
+ const {
+ activeTab,
+ setActiveTab,
+ isDarkMode,
+ toggleDarkMode,
+ isSaving,
+ saveStatus,
+ 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,
+ } = useSettingsController({
+ isOpen,
+ initialTab,
+ projects,
+ onClose,
+ });
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const isAuthenticated = loginProvider === 'claude'
+ ? claudeAuthStatus.authenticated
+ : loginProvider === 'cursor'
+ ? cursorAuthStatus.authenticated
+ : loginProvider === 'codex'
+ ? codexAuthStatus.authenticated
+ : false;
+
+ return (
+
+
+
+
+
+
{t('title')}
+
+
+
+
+
+
+
+
+ {activeTab === 'appearance' && (
+
updateCodeEditorSetting('theme', value)}
+ onCodeEditorWordWrapChange={(value) => updateCodeEditorSetting('wordWrap', value)}
+ onCodeEditorShowMinimapChange={(value) => updateCodeEditorSetting('showMinimap', value)}
+ onCodeEditorLineNumbersChange={(value) => updateCodeEditorSetting('lineNumbers', value)}
+ onCodeEditorFontSizeChange={(value) => updateCodeEditorSetting('fontSize', value)}
+ />
+ )}
+
+ {activeTab === 'git' && }
+
+ {activeTab === 'agents' && (
+ openLoginForProvider('claude')}
+ onCursorLogin={() => openLoginForProvider('cursor')}
+ onCodexLogin={() => openLoginForProvider('codex')}
+ claudePermissions={claudePermissions}
+ onClaudePermissionsChange={setClaudePermissions}
+ cursorPermissions={cursorPermissions}
+ onCursorPermissionsChange={setCursorPermissions}
+ codexPermissionMode={codexPermissionMode}
+ onCodexPermissionModeChange={setCodexPermissionMode}
+ mcpServers={mcpServers}
+ cursorMcpServers={cursorMcpServers}
+ codexMcpServers={codexMcpServers}
+ mcpTestResults={mcpTestResults}
+ mcpServerTools={mcpServerTools}
+ mcpToolsLoading={mcpToolsLoading}
+ onOpenMcpForm={openMcpForm}
+ onDeleteMcpServer={handleMcpDelete}
+ onTestMcpServer={handleMcpTest}
+ onDiscoverMcpTools={handleMcpToolsDiscovery}
+ onOpenCodexMcpForm={openCodexMcpForm}
+ onDeleteCodexMcpServer={handleCodexMcpDelete}
+ />
+ )}
+
+ {activeTab === 'tasks' && (
+
+
+
+ )}
+
+ {activeTab === 'api' && (
+
+
+
+ )}
+
+
+
+
+
+ {saveStatus === 'success' && (
+
+
+ {t('saveStatus.success')}
+
+ )}
+ {saveStatus === 'error' && (
+
+
+ {t('saveStatus.error')}
+
+ )}
+
+
+
+
+
+
+
+
+
setShowLoginModal(false)}
+ provider={loginProvider}
+ project={selectedProject}
+ onComplete={handleLoginComplete}
+ isAuthenticated={isAuthenticated}
+ />
+
+
+
+
+
+ );
+}
+
+export default Settings;
diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts
new file mode 100644
index 0000000..dd11171
--- /dev/null
+++ b/src/components/settings/constants/constants.ts
@@ -0,0 +1,94 @@
+import type {
+ AgentCategory,
+ AgentProvider,
+ AuthStatus,
+ ClaudeMcpFormState,
+ CodexMcpFormState,
+ CodeEditorSettingsState,
+ CursorPermissionsState,
+ McpToolsResult,
+ McpTestResult,
+ ProjectSortOrder,
+ SettingsMainTab,
+} from '../types/types';
+
+export const SETTINGS_MAIN_TABS: SettingsMainTab[] = [
+ 'agents',
+ 'appearance',
+ 'git',
+ 'api',
+ 'tasks',
+];
+
+export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex'];
+export const AGENT_CATEGORIES: AgentCategory[] = ['account', 'permissions', 'mcp'];
+
+export const DEFAULT_PROJECT_SORT_ORDER: ProjectSortOrder = 'name';
+export const DEFAULT_SAVE_STATUS = null;
+export const DEFAULT_CODE_EDITOR_SETTINGS: CodeEditorSettingsState = {
+ theme: 'dark',
+ wordWrap: false,
+ showMinimap: true,
+ lineNumbers: true,
+ fontSize: '14',
+};
+
+export const DEFAULT_AUTH_STATUS: AuthStatus = {
+ authenticated: false,
+ email: null,
+ loading: true,
+ error: null,
+};
+
+export const DEFAULT_MCP_TEST_RESULT: McpTestResult = {
+ success: false,
+ message: '',
+ details: [],
+ loading: false,
+};
+
+export const DEFAULT_MCP_TOOLS_RESULT: McpToolsResult = {
+ success: false,
+ tools: [],
+ resources: [],
+ prompts: [],
+};
+
+export const DEFAULT_CLAUDE_MCP_FORM: ClaudeMcpFormState = {
+ name: '',
+ type: 'stdio',
+ scope: 'user',
+ projectPath: '',
+ config: {
+ command: '',
+ args: [],
+ env: {},
+ url: '',
+ headers: {},
+ timeout: 30000,
+ },
+ importMode: 'form',
+ jsonInput: '',
+};
+
+export const DEFAULT_CODEX_MCP_FORM: CodexMcpFormState = {
+ name: '',
+ type: 'stdio',
+ config: {
+ command: '',
+ args: [],
+ env: {},
+ },
+};
+
+export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = {
+ allowedCommands: [],
+ disallowedCommands: [],
+ skipPermissions: false,
+};
+
+export const AUTH_STATUS_ENDPOINTS: Record = {
+ claude: '/api/cli/claude/status',
+ cursor: '/api/cli/cursor/status',
+ codex: '/api/cli/codex/status',
+};
diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts
new file mode 100644
index 0000000..de7b061
--- /dev/null
+++ b/src/components/settings/hooks/useSettingsController.ts
@@ -0,0 +1,802 @@
+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;
+ url?: string;
+ headers?: Record;
+};
+
+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 = (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 (response: Response): Promise => response.json() as Promise;
+
+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(null);
+
+ const [activeTab, setActiveTab] = useState(() => normalizeMainTab(initialTab));
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null);
+ const [projectSortOrder, setProjectSortOrder] = useState('name');
+ const [codeEditorSettings, setCodeEditorSettings] = useState(() => (
+ readCodeEditorSettings()
+ ));
+
+ const [claudePermissions, setClaudePermissions] = useState(() => (
+ createEmptyClaudePermissions()
+ ));
+ const [cursorPermissions, setCursorPermissions] = useState(() => (
+ createEmptyCursorPermissions()
+ ));
+ const [codexPermissionMode, setCodexPermissionMode] = useState('default');
+
+ const [mcpServers, setMcpServers] = useState([]);
+ const [cursorMcpServers, setCursorMcpServers] = useState([]);
+ const [codexMcpServers, setCodexMcpServers] = useState([]);
+ const [mcpTestResults, setMcpTestResults] = useState>({});
+ const [mcpServerTools, setMcpServerTools] = useState>({});
+ const [mcpToolsLoading, setMcpToolsLoading] = useState>({});
+
+ const [showMcpForm, setShowMcpForm] = useState(false);
+ const [editingMcpServer, setEditingMcpServer] = useState(null);
+ const [showCodexMcpForm, setShowCodexMcpForm] = useState(false);
+ const [editingCodexMcpServer, setEditingCodexMcpServer] = useState(null);
+
+ const [showLoginModal, setShowLoginModal] = useState(false);
+ const [loginProvider, setLoginProvider] = useState('');
+ const [selectedProject, setSelectedProject] = useState(null);
+
+ const [claudeAuthStatus, setClaudeAuthStatus] = useState(DEFAULT_AUTH_STATUS);
+ const [cursorAuthStatus, setCursorAuthStatus] = useState(DEFAULT_AUTH_STATUS);
+ const [codexAuthStatus, setCodexAuthStatus] = useState(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(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(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(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(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(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(response);
+ throw new Error(error.error || 'Failed to delete server');
+ }
+
+ const result = await toResponseJson(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) => {
+ if (editingServer?.id) {
+ // Editing still follows the existing behavior: remove current entry and re-create it.
+ await deleteMcpServer(editingServer.id, editingServer.scope || 'user');
+ }
+
+ const response = await authenticatedFetch('/api/mcp/cli/add', {
+ method: 'POST',
+ body: JSON.stringify({
+ name: serverData.name,
+ type: serverData.type,
+ scope: serverData.scope,
+ 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(response);
+ throw new Error(error.error || 'Failed to save server');
+ }
+
+ const result = await toResponseJson(response);
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to save server via Claude CLI');
+ }
+ },
+ [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(response);
+ throw new Error(error.error || 'Failed to add server');
+ }
+
+ const result = await toResponseJson(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;
+ }
+
+ try {
+ await deleteMcpServer(serverId, scope);
+ await fetchMcpServers();
+ setSaveStatus('success');
+ } catch (error) {
+ alert(`Error: ${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(response);
+ throw new Error(error.error || 'Failed to test server');
+ }
+
+ const data = await toResponseJson(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(response);
+ throw new Error(error.error || 'Failed to discover tools');
+ }
+
+ const data = await toResponseJson(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(response);
+ throw new Error(error.error || 'Failed to delete server');
+ }
+
+ const result = await toResponseJson(response);
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to delete Codex MCP server');
+ }
+ }, []);
+
+ const saveCodexMcpServer = useCallback(
+ async (serverData: CodexMcpFormState, editingServer: McpServer | null) => {
+ if (editingServer?.name) {
+ await deleteCodexMcpServer(editingServer.name);
+ }
+
+ 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(response);
+ throw new Error(error.error || 'Failed to save server');
+ }
+
+ const result = await toResponseJson(response);
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to save Codex MCP server');
+ }
+ },
+ [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;
+ }
+
+ try {
+ await deleteCodexMcpServer(serverName);
+ await fetchCodexMcpServers();
+ setSaveStatus('success');
+ } catch (error) {
+ alert(`Error: ${getErrorMessage(error)}`);
+ setSaveStatus('error');
+ }
+ },
+ [deleteCodexMcpServer, fetchCodexMcpServers],
+ );
+
+ const loadSettings = useCallback(async () => {
+ try {
+ const savedClaudeSettings = parseJson(
+ localStorage.getItem('claude-settings'),
+ {},
+ );
+ setClaudePermissions({
+ allowedTools: savedClaudeSettings.allowedTools || [],
+ disallowedTools: savedClaudeSettings.disallowedTools || [],
+ skipPermissions: Boolean(savedClaudeSettings.skipPermissions),
+ });
+ setProjectSortOrder(savedClaudeSettings.projectSortOrder === 'date' ? 'date' : 'name');
+
+ const savedCursorSettings = parseJson(
+ localStorage.getItem('cursor-tools-settings'),
+ {},
+ );
+ setCursorPermissions({
+ allowedCommands: savedCursorSettings.allowedCommands || [],
+ disallowedCommands: savedCursorSettings.disallowedCommands || [],
+ skipPermissions: Boolean(savedCursorSettings.skipPermissions),
+ });
+
+ const savedCodexSettings = parseJson(
+ 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');
+ 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(
+ (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);
+ }
+ }, []);
+
+ return {
+ activeTab,
+ setActiveTab,
+ isDarkMode,
+ toggleDarkMode,
+ isSaving,
+ saveStatus,
+ 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,
+ };
+}
diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts
new file mode 100644
index 0000000..a0071c8
--- /dev/null
+++ b/src/components/settings/types/types.ts
@@ -0,0 +1,134 @@
+import type { Dispatch, SetStateAction } from 'react';
+
+export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks';
+export type AgentProvider = 'claude' | 'cursor' | 'codex';
+export type AgentCategory = 'account' | 'permissions' | 'mcp';
+export type ProjectSortOrder = 'name' | 'date';
+export type SaveStatus = 'success' | 'error' | null;
+export type CodexPermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
+export type McpImportMode = 'form' | 'json';
+export type McpScope = 'user' | 'local';
+export type McpTransportType = 'stdio' | 'sse' | 'http';
+
+export type SettingsProject = {
+ name: string;
+ displayName?: string;
+ fullPath?: string;
+ path?: string;
+};
+
+export type AuthStatus = {
+ authenticated: boolean;
+ email: string | null;
+ loading: boolean;
+ error: string | null;
+};
+
+export type KeyValueMap = Record;
+
+export type McpServerConfig = {
+ command?: string;
+ args?: string[];
+ env?: KeyValueMap;
+ url?: string;
+ headers?: KeyValueMap;
+ timeout?: number;
+};
+
+export type McpServer = {
+ id?: string;
+ name: string;
+ type?: string;
+ scope?: string;
+ projectPath?: string;
+ config?: McpServerConfig;
+ raw?: unknown;
+ created?: string;
+ updated?: string;
+};
+
+export type ClaudeMcpFormConfig = {
+ command: string;
+ args: string[];
+ env: KeyValueMap;
+ url: string;
+ headers: KeyValueMap;
+ timeout: number;
+};
+
+export type ClaudeMcpFormState = {
+ name: string;
+ type: McpTransportType;
+ scope: McpScope;
+ projectPath: string;
+ config: ClaudeMcpFormConfig;
+ importMode: McpImportMode;
+ jsonInput: string;
+ raw?: unknown;
+};
+
+export type CodexMcpFormConfig = {
+ command: string;
+ args: string[];
+ env: KeyValueMap;
+};
+
+export type CodexMcpFormState = {
+ name: string;
+ type: 'stdio';
+ config: CodexMcpFormConfig;
+};
+
+export type McpTestResult = {
+ success: boolean;
+ message: string;
+ details?: string[];
+ loading?: boolean;
+};
+
+export type McpTool = {
+ name: string;
+ [key: string]: unknown;
+};
+
+export type McpToolsResult = {
+ success?: boolean;
+ tools?: McpTool[];
+ resources?: unknown[];
+ prompts?: unknown[];
+};
+
+export type ClaudePermissionsState = {
+ allowedTools: string[];
+ disallowedTools: string[];
+ skipPermissions: boolean;
+};
+
+export type CursorPermissionsState = {
+ allowedCommands: string[];
+ disallowedCommands: string[];
+ skipPermissions: boolean;
+};
+
+export type CodeEditorSettingsState = {
+ theme: 'dark' | 'light';
+ wordWrap: boolean;
+ showMinimap: boolean;
+ lineNumbers: boolean;
+ fontSize: string;
+};
+
+export type SettingsStoragePayload = {
+ claude: ClaudePermissionsState & { projectSortOrder: ProjectSortOrder; lastUpdated: string };
+ cursor: CursorPermissionsState & { lastUpdated: string };
+ codex: { permissionMode: CodexPermissionMode; lastUpdated: string };
+};
+
+export type SettingsProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ projects?: SettingsProject[];
+ initialTab?: string;
+};
+
+export type SetState = Dispatch>;
diff --git a/src/components/settings/view/SettingsMainTabs.tsx b/src/components/settings/view/SettingsMainTabs.tsx
new file mode 100644
index 0000000..cce1e30
--- /dev/null
+++ b/src/components/settings/view/SettingsMainTabs.tsx
@@ -0,0 +1,52 @@
+import { GitBranch, Key } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import type { SettingsMainTab } from '../types/types';
+
+type SettingsMainTabsProps = {
+ activeTab: SettingsMainTab;
+ onChange: (tab: SettingsMainTab) => void;
+};
+
+type MainTabConfig = {
+ id: SettingsMainTab;
+ labelKey: string;
+ icon?: typeof GitBranch;
+};
+
+const TAB_CONFIG: MainTabConfig[] = [
+ { id: 'agents', labelKey: 'mainTabs.agents' },
+ { id: 'appearance', labelKey: 'mainTabs.appearance' },
+ { id: 'git', labelKey: 'mainTabs.git', icon: GitBranch },
+ { id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key },
+ { id: 'tasks', labelKey: 'mainTabs.tasks' },
+];
+
+export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTabsProps) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+ {TAB_CONFIG.map((tab) => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/settings/view/modals/ClaudeMcpFormModal.tsx b/src/components/settings/view/modals/ClaudeMcpFormModal.tsx
new file mode 100644
index 0000000..44e3114
--- /dev/null
+++ b/src/components/settings/view/modals/ClaudeMcpFormModal.tsx
@@ -0,0 +1,479 @@
+import { FolderOpen, Globe, X } from 'lucide-react';
+import { useEffect, useMemo, useState } from 'react';
+import type { FormEvent } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Input } from '../../../ui/input';
+import { Button } from '../../../ui/button';
+import { DEFAULT_CLAUDE_MCP_FORM } from '../../constants/constants';
+import type { ClaudeMcpFormState, McpServer, McpScope, McpTransportType, SettingsProject } from '../../types/types';
+
+type ClaudeMcpFormModalProps = {
+ isOpen: boolean;
+ editingServer: McpServer | null;
+ projects: SettingsProject[];
+ onClose: () => void;
+ onSubmit: (formData: ClaudeMcpFormState, editingServer: McpServer | null) => Promise;
+};
+
+const getSafeTransportType = (value: unknown): McpTransportType => {
+ if (value === 'sse' || value === 'http') {
+ return value;
+ }
+
+ return 'stdio';
+};
+
+const getSafeScope = (value: unknown): McpScope => (value === 'local' ? 'local' : 'user');
+
+const getErrorMessage = (error: unknown): string => (
+ error instanceof Error ? error.message : 'Unknown error'
+);
+
+const createFormStateFromServer = (server: McpServer): ClaudeMcpFormState => ({
+ name: server.name || '',
+ type: getSafeTransportType(server.type),
+ scope: getSafeScope(server.scope),
+ projectPath: server.projectPath || '',
+ config: {
+ command: server.config?.command || '',
+ args: server.config?.args || [],
+ env: server.config?.env || {},
+ url: server.config?.url || '',
+ headers: server.config?.headers || {},
+ timeout: server.config?.timeout || 30000,
+ },
+ importMode: 'form',
+ jsonInput: '',
+ raw: server.raw,
+});
+
+export default function ClaudeMcpFormModal({
+ isOpen,
+ editingServer,
+ projects,
+ onClose,
+ onSubmit,
+}: ClaudeMcpFormModalProps) {
+ const { t } = useTranslation('settings');
+ const [formData, setFormData] = useState(DEFAULT_CLAUDE_MCP_FORM);
+ const [jsonValidationError, setJsonValidationError] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const isEditing = Boolean(editingServer);
+
+ useEffect(() => {
+ if (!isOpen) {
+ return;
+ }
+
+ setJsonValidationError('');
+ if (editingServer) {
+ setFormData(createFormStateFromServer(editingServer));
+ return;
+ }
+
+ setFormData(DEFAULT_CLAUDE_MCP_FORM);
+ }, [editingServer, isOpen]);
+
+ const canSubmit = useMemo(() => {
+ if (!formData.name.trim()) {
+ return false;
+ }
+
+ if (formData.importMode === 'json') {
+ return Boolean(formData.jsonInput.trim()) && !jsonValidationError;
+ }
+
+ if (formData.scope === 'local' && !formData.projectPath.trim()) {
+ return false;
+ }
+
+ if (formData.type === 'stdio') {
+ return Boolean(formData.config.command.trim());
+ }
+
+ return Boolean(formData.config.url.trim());
+ }, [formData, jsonValidationError]);
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const updateConfig = (
+ key: K,
+ value: ClaudeMcpFormState['config'][K],
+ ) => {
+ setFormData((prev) => ({
+ ...prev,
+ config: {
+ ...prev.config,
+ [key]: value,
+ },
+ }));
+ };
+
+ const handleJsonValidation = (value: string) => {
+ if (!value.trim()) {
+ setJsonValidationError('');
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(value) as { type?: string; command?: string; url?: string };
+ if (!parsed.type) {
+ setJsonValidationError(t('mcpForm.validation.missingType'));
+ } else if (parsed.type === 'stdio' && !parsed.command) {
+ setJsonValidationError(t('mcpForm.validation.stdioRequiresCommand'));
+ } else if ((parsed.type === 'http' || parsed.type === 'sse') && !parsed.url) {
+ setJsonValidationError(t('mcpForm.validation.httpRequiresUrl', { type: parsed.type }));
+ } else {
+ setJsonValidationError('');
+ }
+ } catch {
+ setJsonValidationError(t('mcpForm.validation.invalidJson'));
+ }
+ };
+
+ const handleSubmit = async (event: FormEvent) => {
+ event.preventDefault();
+ setIsSubmitting(true);
+
+ try {
+ await onSubmit(formData, editingServer);
+ } catch (error) {
+ alert(`Error: ${getErrorMessage(error)}`);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+ {isEditing ? t('mcpForm.title.edit') : t('mcpForm.title.add')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/view/modals/CodexMcpFormModal.tsx b/src/components/settings/view/modals/CodexMcpFormModal.tsx
new file mode 100644
index 0000000..6e34389
--- /dev/null
+++ b/src/components/settings/view/modals/CodexMcpFormModal.tsx
@@ -0,0 +1,178 @@
+import { useEffect, useState } from 'react';
+import type { FormEvent } from 'react';
+import { X } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Button } from '../../../ui/button';
+import { Input } from '../../../ui/input';
+import { DEFAULT_CODEX_MCP_FORM } from '../../constants/constants';
+import type { CodexMcpFormState, McpServer } from '../../types/types';
+
+type CodexMcpFormModalProps = {
+ isOpen: boolean;
+ editingServer: McpServer | null;
+ onClose: () => void;
+ onSubmit: (formData: CodexMcpFormState, editingServer: McpServer | null) => Promise;
+};
+
+const getErrorMessage = (error: unknown): string => (
+ error instanceof Error ? error.message : 'Unknown error'
+);
+
+const createFormStateFromServer = (server: McpServer): CodexMcpFormState => ({
+ name: server.name || '',
+ type: 'stdio',
+ config: {
+ command: server.config?.command || '',
+ args: server.config?.args || [],
+ env: server.config?.env || {},
+ },
+});
+
+export default function CodexMcpFormModal({
+ isOpen,
+ editingServer,
+ onClose,
+ onSubmit,
+}: CodexMcpFormModalProps) {
+ const { t } = useTranslation('settings');
+ const [formData, setFormData] = useState(DEFAULT_CODEX_MCP_FORM);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (!isOpen) {
+ return;
+ }
+
+ if (editingServer) {
+ setFormData(createFormStateFromServer(editingServer));
+ return;
+ }
+
+ setFormData(DEFAULT_CODEX_MCP_FORM);
+ }, [editingServer, isOpen]);
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const handleSubmit = async (event: FormEvent) => {
+ event.preventDefault();
+ setIsSubmitting(true);
+
+ try {
+ await onSubmit(formData, editingServer);
+ } catch (error) {
+ alert(`Error: ${getErrorMessage(error)}`);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+ {editingServer ? t('mcpForm.title.edit') : t('mcpForm.title.add')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/view/tabs/AppearanceSettingsTab.tsx b/src/components/settings/view/tabs/AppearanceSettingsTab.tsx
new file mode 100644
index 0000000..7cc0d08
--- /dev/null
+++ b/src/components/settings/view/tabs/AppearanceSettingsTab.tsx
@@ -0,0 +1,189 @@
+import { Moon, Sun } from 'lucide-react';
+import type { ReactNode } from 'react';
+import { useTranslation } from 'react-i18next';
+import LanguageSelector from '../../../LanguageSelector';
+import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types';
+
+type AppearanceSettingsTabProps = {
+ isDarkMode: boolean;
+ onToggleDarkMode: () => void;
+ projectSortOrder: ProjectSortOrder;
+ onProjectSortOrderChange: (value: ProjectSortOrder) => void;
+ codeEditorSettings: CodeEditorSettingsState;
+ onCodeEditorThemeChange: (value: 'dark' | 'light') => void;
+ onCodeEditorWordWrapChange: (value: boolean) => void;
+ onCodeEditorShowMinimapChange: (value: boolean) => void;
+ onCodeEditorLineNumbersChange: (value: boolean) => void;
+ onCodeEditorFontSizeChange: (value: string) => void;
+};
+
+type ToggleCardProps = {
+ label: string;
+ description: string;
+ checked: boolean;
+ onChange: (value: boolean) => void;
+ onIcon?: ReactNode;
+ offIcon?: ReactNode;
+ ariaLabel: string;
+};
+
+function ToggleCard({
+ label,
+ description,
+ checked,
+ onChange,
+ onIcon,
+ offIcon,
+ ariaLabel,
+}: ToggleCardProps) {
+ return (
+
+
+
+
{label}
+
{description}
+
+
+
+
+ );
+}
+
+export default function AppearanceSettingsTab({
+ isDarkMode,
+ onToggleDarkMode,
+ projectSortOrder,
+ onProjectSortOrderChange,
+ codeEditorSettings,
+ onCodeEditorThemeChange,
+ onCodeEditorWordWrapChange,
+ onCodeEditorShowMinimapChange,
+ onCodeEditorLineNumbersChange,
+ onCodeEditorFontSizeChange,
+}: AppearanceSettingsTabProps) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+ }
+ offIcon={}
+ ariaLabel={t('appearanceSettings.darkMode.label')}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {t('appearanceSettings.projectSorting.label')}
+
+
+ {t('appearanceSettings.projectSorting.description')}
+
+
+
+
+
+
+
+
+
{t('appearanceSettings.codeEditor.title')}
+
+
onCodeEditorThemeChange(enabled ? 'dark' : 'light')}
+ onIcon={}
+ offIcon={}
+ ariaLabel={t('appearanceSettings.codeEditor.theme.label')}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {t('appearanceSettings.codeEditor.fontSize.label')}
+
+
+ {t('appearanceSettings.codeEditor.fontSize.description')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/AccountContent.jsx b/src/components/settings/view/tabs/agents-settings/AccountContent.tsx
similarity index 73%
rename from src/components/settings/AccountContent.jsx
rename to src/components/settings/view/tabs/agents-settings/AccountContent.tsx
index cad2e2a..9dcd735 100644
--- a/src/components/settings/AccountContent.jsx
+++ b/src/components/settings/view/tabs/agents-settings/AccountContent.tsx
@@ -1,13 +1,28 @@
-import { Button } from '../ui/button';
-import { Badge } from '../ui/badge';
import { LogIn } from 'lucide-react';
-import SessionProviderLogo from '../SessionProviderLogo';
import { useTranslation } from 'react-i18next';
+import { Badge } from '../../../../ui/badge';
+import { Button } from '../../../../ui/button';
+import SessionProviderLogo from '../../../../SessionProviderLogo';
+import type { AgentProvider, AuthStatus } from '../../../types/types';
-const agentConfig = {
+type AccountContentProps = {
+ agent: AgentProvider;
+ authStatus: AuthStatus;
+ onLogin: () => void;
+};
+
+type AgentVisualConfig = {
+ name: string;
+ bgClass: string;
+ borderClass: string;
+ textClass: string;
+ subtextClass: string;
+ buttonClass: string;
+};
+
+const agentConfig: Record = {
claude: {
name: 'Claude',
- description: 'Anthropic Claude AI assistant',
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
borderClass: 'border-blue-200 dark:border-blue-800',
textClass: 'text-blue-900 dark:text-blue-100',
@@ -16,7 +31,6 @@ const agentConfig = {
},
cursor: {
name: 'Cursor',
- description: 'Cursor AI-powered code editor',
bgClass: 'bg-purple-50 dark:bg-purple-900/20',
borderClass: 'border-purple-200 dark:border-purple-800',
textClass: 'text-purple-900 dark:text-purple-100',
@@ -25,7 +39,6 @@ const agentConfig = {
},
codex: {
name: 'Codex',
- description: 'OpenAI Codex AI assistant',
bgClass: 'bg-gray-100 dark:bg-gray-800/50',
borderClass: 'border-gray-300 dark:border-gray-600',
textClass: 'text-gray-900 dark:text-gray-100',
@@ -34,7 +47,7 @@ const agentConfig = {
},
};
-export default function AccountContent({ agent, authStatus, onLogin }) {
+export default function AccountContent({ agent, authStatus, onLogin }: AccountContentProps) {
const { t } = useTranslation('settings');
const config = agentConfig[agent];
@@ -50,29 +63,30 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
- {/* Connection Status */}
{t('agents.connectionStatus')}
- {authStatus?.loading ? (
+ {authStatus.loading ? (
t('agents.authStatus.checkingAuth')
- ) : authStatus?.authenticated ? (
- t('agents.authStatus.loggedInAs', { email: authStatus.email || t('agents.authStatus.authenticatedUser') })
+ ) : authStatus.authenticated ? (
+ t('agents.authStatus.loggedInAs', {
+ email: authStatus.email || t('agents.authStatus.authenticatedUser'),
+ })
) : (
t('agents.authStatus.notConnected')
)}
- {authStatus?.loading ? (
+ {authStatus.loading ? (
{t('agents.authStatus.checking')}
- ) : authStatus?.authenticated ? (
-
+ ) : authStatus.authenticated ? (
+
{t('agents.authStatus.connected')}
) : (
@@ -87,10 +101,10 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
- {authStatus?.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
+ {authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
- {authStatus?.authenticated
+ {authStatus.authenticated
? t('agents.login.reAuthDescription')
: t('agents.login.description', { agent: config.name })}
@@ -101,12 +115,12 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
size="sm"
>
- {authStatus?.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
+ {authStatus.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
- {authStatus?.error && (
+ {authStatus.error && (
{t('agents.error', { error: authStatus.error })}
diff --git a/src/components/settings/AgentListItem.jsx b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
similarity index 79%
rename from src/components/settings/AgentListItem.jsx
rename to src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
index babd8b5..eea803e 100644
--- a/src/components/settings/AgentListItem.jsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
@@ -1,7 +1,21 @@
-import SessionProviderLogo from '../SessionProviderLogo';
import { useTranslation } from 'react-i18next';
+import SessionProviderLogo from '../../../../SessionProviderLogo';
+import type { AgentProvider, AuthStatus } from '../../../types/types';
-const agentConfig = {
+type AgentListItemProps = {
+ agentId: AgentProvider;
+ authStatus: AuthStatus;
+ isSelected: boolean;
+ onClick: () => void;
+ isMobile?: boolean;
+};
+
+type AgentConfig = {
+ name: string;
+ color: 'blue' | 'purple' | 'gray';
+};
+
+const agentConfig: Record
= {
claude: {
name: 'Claude',
color: 'blue',
@@ -35,14 +49,19 @@ const colorClasses = {
bg: 'bg-gray-100 dark:bg-gray-800/50',
dot: 'bg-gray-700 dark:bg-gray-300',
},
-};
+} as const;
-export default function AgentListItem({ agentId, authStatus, isSelected, onClick, isMobile = false }) {
+export default function AgentListItem({
+ agentId,
+ authStatus,
+ isSelected,
+ onClick,
+ isMobile = false,
+}: AgentListItemProps) {
const { t } = useTranslation('settings');
const config = agentConfig[agentId];
const colors = colorClasses[config.color];
- // Mobile: horizontal layout with bottom border
if (isMobile) {
return (
@@ -64,7 +83,6 @@ export default function AgentListItem({ agentId, authStatus, isSelected, onClick
);
}
- // Desktop: vertical layout with left border
return (
- {authStatus?.loading ? (
+ {authStatus.loading ? (
{t('agents.authStatus.checking')}
- ) : authStatus?.authenticated ? (
+ ) : authStatus.authenticated ? (
-
+
{authStatus.email || t('agents.authStatus.connected')}
diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
new file mode 100644
index 0000000..2101e9e
--- /dev/null
+++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
@@ -0,0 +1,248 @@
+import { useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import AgentListItem from './AgentListItem';
+import AccountContent from './AccountContent';
+import McpServersContent from './McpServersContent';
+import PermissionsContent from './PermissionsContent';
+import type {
+ AgentCategory,
+ AgentProvider,
+ AuthStatus,
+ ClaudePermissionsState,
+ CodexPermissionMode,
+ CursorPermissionsState,
+ McpServer,
+ McpToolsResult,
+ McpTestResult,
+} from '../../../types/types';
+
+type AgentsSettingsTabProps = {
+ claudeAuthStatus: AuthStatus;
+ cursorAuthStatus: AuthStatus;
+ codexAuthStatus: AuthStatus;
+ onClaudeLogin: () => void;
+ onCursorLogin: () => void;
+ onCodexLogin: () => void;
+ claudePermissions: ClaudePermissionsState;
+ onClaudePermissionsChange: (value: ClaudePermissionsState) => void;
+ cursorPermissions: CursorPermissionsState;
+ onCursorPermissionsChange: (value: CursorPermissionsState) => void;
+ codexPermissionMode: CodexPermissionMode;
+ onCodexPermissionModeChange: (value: CodexPermissionMode) => void;
+ mcpServers: McpServer[];
+ cursorMcpServers: McpServer[];
+ codexMcpServers: McpServer[];
+ mcpTestResults: Record
;
+ mcpServerTools: Record;
+ mcpToolsLoading: Record;
+ onOpenMcpForm: (server?: McpServer) => void;
+ onDeleteMcpServer: (serverId: string, scope?: string) => void;
+ onTestMcpServer: (serverId: string, scope?: string) => void;
+ onDiscoverMcpTools: (serverId: string, scope?: string) => void;
+ onOpenCodexMcpForm: (server?: McpServer) => void;
+ onDeleteCodexMcpServer: (serverId: string) => void;
+};
+
+type AgentContext = {
+ authStatus: AuthStatus;
+ onLogin: () => void;
+};
+
+export default function AgentsSettingsTab({
+ claudeAuthStatus,
+ cursorAuthStatus,
+ codexAuthStatus,
+ onClaudeLogin,
+ onCursorLogin,
+ onCodexLogin,
+ claudePermissions,
+ onClaudePermissionsChange,
+ cursorPermissions,
+ onCursorPermissionsChange,
+ codexPermissionMode,
+ onCodexPermissionModeChange,
+ mcpServers,
+ cursorMcpServers,
+ codexMcpServers,
+ mcpTestResults,
+ mcpServerTools,
+ mcpToolsLoading,
+ onOpenMcpForm,
+ onDeleteMcpServer,
+ onTestMcpServer,
+ onDiscoverMcpTools,
+ onOpenCodexMcpForm,
+ onDeleteCodexMcpServer,
+}: AgentsSettingsTabProps) {
+ const { t } = useTranslation('settings');
+ const [selectedAgent, setSelectedAgent] = useState('claude');
+ const [selectedCategory, setSelectedCategory] = useState('account');
+ // Cursor MCP add/edit/delete was previously a placeholder and is intentionally preserved.
+ const noopCursorMcpAction = () => {};
+
+ const agentContextById = useMemo>(() => ({
+ claude: {
+ authStatus: claudeAuthStatus,
+ onLogin: onClaudeLogin,
+ },
+ cursor: {
+ authStatus: cursorAuthStatus,
+ onLogin: onCursorLogin,
+ },
+ codex: {
+ authStatus: codexAuthStatus,
+ onLogin: onCodexLogin,
+ },
+ }), [
+ claudeAuthStatus,
+ codexAuthStatus,
+ cursorAuthStatus,
+ onClaudeLogin,
+ onCodexLogin,
+ onCursorLogin,
+ ]);
+
+ return (
+
+
+
+ {(['claude', 'cursor', 'codex'] as AgentProvider[]).map((agent) => (
+
setSelectedAgent(agent)}
+ isMobile
+ />
+ ))}
+
+
+
+
+
+ {(['claude', 'cursor', 'codex'] as AgentProvider[]).map((agent) => (
+
setSelectedAgent(agent)}
+ />
+ ))}
+
+
+
+
+
+
+ {(['account', 'permissions', 'mcp'] as AgentCategory[]).map((category) => (
+
+ ))}
+
+
+
+
+ {selectedCategory === 'account' && (
+
+ )}
+
+ {selectedCategory === 'permissions' && selectedAgent === 'claude' && (
+
{
+ onClaudePermissionsChange({ ...claudePermissions, skipPermissions: value });
+ }}
+ allowedTools={claudePermissions.allowedTools}
+ onAllowedToolsChange={(value) => {
+ onClaudePermissionsChange({ ...claudePermissions, allowedTools: value });
+ }}
+ disallowedTools={claudePermissions.disallowedTools}
+ onDisallowedToolsChange={(value) => {
+ onClaudePermissionsChange({ ...claudePermissions, disallowedTools: value });
+ }}
+ />
+ )}
+
+ {selectedCategory === 'permissions' && selectedAgent === 'cursor' && (
+ {
+ onCursorPermissionsChange({ ...cursorPermissions, skipPermissions: value });
+ }}
+ allowedCommands={cursorPermissions.allowedCommands}
+ onAllowedCommandsChange={(value) => {
+ onCursorPermissionsChange({ ...cursorPermissions, allowedCommands: value });
+ }}
+ disallowedCommands={cursorPermissions.disallowedCommands}
+ onDisallowedCommandsChange={(value) => {
+ onCursorPermissionsChange({ ...cursorPermissions, disallowedCommands: value });
+ }}
+ />
+ )}
+
+ {selectedCategory === 'permissions' && selectedAgent === 'codex' && (
+
+ )}
+
+ {selectedCategory === 'mcp' && selectedAgent === 'claude' && (
+ onOpenMcpForm()}
+ onEdit={(server) => onOpenMcpForm(server)}
+ onDelete={onDeleteMcpServer}
+ onTest={onTestMcpServer}
+ onDiscoverTools={onDiscoverMcpTools}
+ testResults={mcpTestResults}
+ serverTools={mcpServerTools}
+ toolsLoading={mcpToolsLoading}
+ />
+ )}
+
+ {selectedCategory === 'mcp' && selectedAgent === 'cursor' && (
+
+ )}
+
+ {selectedCategory === 'mcp' && selectedAgent === 'codex' && (
+ onOpenCodexMcpForm()}
+ onEdit={(server) => onOpenCodexMcpForm(server)}
+ onDelete={(serverId) => onDeleteCodexMcpServer(serverId)}
+ />
+ )}
+
+
+
+ );
+}
diff --git a/src/components/settings/view/tabs/agents-settings/McpServersContent.tsx b/src/components/settings/view/tabs/agents-settings/McpServersContent.tsx
new file mode 100644
index 0000000..ca9ba95
--- /dev/null
+++ b/src/components/settings/view/tabs/agents-settings/McpServersContent.tsx
@@ -0,0 +1,360 @@
+import { Edit3, Globe, Plus, Server, Terminal, Trash2, Zap } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Badge } from '../../../../ui/badge';
+import { Button } from '../../../../ui/button';
+import type { McpServer, McpToolsResult, McpTestResult } from '../../../types/types';
+
+const getTransportIcon = (type: string | undefined) => {
+ if (type === 'stdio') {
+ return ;
+ }
+
+ if (type === 'sse') {
+ return ;
+ }
+
+ if (type === 'http') {
+ return ;
+ }
+
+ return ;
+};
+
+type ClaudeMcpServersProps = {
+ agent: 'claude';
+ servers: McpServer[];
+ onAdd: () => void;
+ onEdit: (server: McpServer) => void;
+ onDelete: (serverId: string, scope?: string) => void;
+ onTest: (serverId: string, scope?: string) => void;
+ onDiscoverTools: (serverId: string, scope?: string) => void;
+ testResults: Record;
+ serverTools: Record;
+ toolsLoading: Record;
+};
+
+function ClaudeMcpServers({
+ servers,
+ onAdd,
+ onEdit,
+ onDelete,
+ testResults,
+ serverTools,
+}: Omit) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+
+
{t('mcpServers.title')}
+
+
{t('mcpServers.description.claude')}
+
+
+
+
+
+
+ {servers.map((server) => {
+ const serverId = server.id || server.name;
+ const testResult = testResults[serverId];
+ const toolsResult = serverTools[serverId];
+
+ return (
+
+
+
+
+ {getTransportIcon(server.type)}
+ {server.name}
+
+ {server.type || 'stdio'}
+
+
+ {server.scope === 'local'
+ ? t('mcpServers.scope.local')
+ : server.scope === 'user'
+ ? t('mcpServers.scope.user')
+ : server.scope}
+
+
+
+
+ {server.type === 'stdio' && server.config?.command && (
+
+ {t('mcpServers.config.command')}:{' '}
+ {server.config.command}
+
+ )}
+ {(server.type === 'sse' || server.type === 'http') && server.config?.url && (
+
+ {t('mcpServers.config.url')}:{' '}
+ {server.config.url}
+
+ )}
+ {server.config?.args && server.config.args.length > 0 && (
+
+ {t('mcpServers.config.args')}:{' '}
+ {server.config.args.join(' ')}
+
+ )}
+
+
+ {testResult && (
+
+ )}
+
+ {toolsResult && toolsResult.tools && toolsResult.tools.length > 0 && (
+
+
+ {t('mcpServers.tools.title')} {t('mcpServers.tools.count', { count: toolsResult.tools.length })}
+
+
+ {toolsResult.tools.slice(0, 5).map((tool, index) => (
+
+ {tool.name}
+
+ ))}
+ {toolsResult.tools.length > 5 && (
+
+ {t('mcpServers.tools.more', { count: toolsResult.tools.length - 5 })}
+
+ )}
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+ })}
+ {servers.length === 0 && (
+
{t('mcpServers.empty')}
+ )}
+
+
+ );
+}
+
+type CursorMcpServersProps = {
+ agent: 'cursor';
+ servers: McpServer[];
+ onAdd: () => void;
+ onEdit: (server: McpServer) => void;
+ onDelete: (serverId: string) => void;
+};
+
+function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+
+
{t('mcpServers.title')}
+
+
{t('mcpServers.description.cursor')}
+
+
+
+
+
+
+ {servers.map((server) => {
+ const serverId = server.id || server.name;
+
+ return (
+
+
+
+
+
+ {server.name}
+ stdio
+
+
+ {server.config?.command && (
+
+ {t('mcpServers.config.command')}:{' '}
+ {server.config.command}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+ })}
+ {servers.length === 0 && (
+
{t('mcpServers.empty')}
+ )}
+
+
+ );
+}
+
+type CodexMcpServersProps = {
+ agent: 'codex';
+ servers: McpServer[];
+ onAdd: () => void;
+ onEdit: (server: McpServer) => void;
+ onDelete: (serverId: string) => void;
+};
+
+function CodexMcpServers({ servers, onAdd, onEdit, onDelete }: Omit) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+
+
{t('mcpServers.title')}
+
+
{t('mcpServers.description.codex')}
+
+
+
+
+
+
+ {servers.map((server) => (
+
+
+
+
+
+ {server.name}
+ stdio
+
+
+
+ {server.config?.command && (
+
+ {t('mcpServers.config.command')}:{' '}
+ {server.config.command}
+
+ )}
+ {server.config?.args && server.config.args.length > 0 && (
+
+ {t('mcpServers.config.args')}:{' '}
+ {server.config.args.join(' ')}
+
+ )}
+ {server.config?.env && Object.keys(server.config.env).length > 0 && (
+
+ {t('mcpServers.config.environment')}:{' '}
+
+ {Object.entries(server.config.env).map(([key, value]) => `${key}=${value}`).join(', ')}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ ))}
+ {servers.length === 0 && (
+
{t('mcpServers.empty')}
+ )}
+
+
+
+
{t('mcpServers.help.title')}
+
{t('mcpServers.help.description')}
+
+
+ );
+}
+
+type McpServersContentProps = ClaudeMcpServersProps | CursorMcpServersProps | CodexMcpServersProps;
+
+export default function McpServersContent(props: McpServersContentProps) {
+ if (props.agent === 'claude') {
+ return ;
+ }
+
+ if (props.agent === 'cursor') {
+ return ;
+ }
+
+ return ;
+}
diff --git a/src/components/settings/PermissionsContent.jsx b/src/components/settings/view/tabs/agents-settings/PermissionsContent.tsx
similarity index 66%
rename from src/components/settings/PermissionsContent.jsx
rename to src/components/settings/view/tabs/agents-settings/PermissionsContent.tsx
index 7bbd53d..5e03772 100644
--- a/src/components/settings/PermissionsContent.jsx
+++ b/src/components/settings/view/tabs/agents-settings/PermissionsContent.tsx
@@ -1,10 +1,11 @@
-import { Button } from '../ui/button';
-import { Input } from '../ui/input';
-import { Shield, AlertTriangle, Plus, X } from 'lucide-react';
+import { useState } from 'react';
+import { AlertTriangle, Plus, Shield, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
+import { Button } from '../../../../ui/button';
+import { Input } from '../../../../ui/input';
+import type { CodexPermissionMode } from '../../../types/types';
-// Common tool patterns for Claude
-const commonClaudeTools = [
+const COMMON_CLAUDE_TOOLS = [
'Bash(git log:*)',
'Bash(git diff:*)',
'Bash(git status:*)',
@@ -18,11 +19,10 @@ const commonClaudeTools = [
'TodoWrite',
'TodoRead',
'WebFetch',
- 'WebSearch'
+ 'WebSearch',
];
-// Common shell commands for Cursor
-const commonCursorCommands = [
+const COMMON_CURSOR_COMMANDS = [
'Shell(ls)',
'Shell(mkdir)',
'Shell(cd)',
@@ -34,61 +34,77 @@ const commonCursorCommands = [
'Shell(npm install)',
'Shell(npm run)',
'Shell(python)',
- 'Shell(node)'
+ 'Shell(node)',
];
-// Claude Permissions
+const addUnique = (items: string[], value: string): string[] => {
+ const normalizedValue = value.trim();
+ if (!normalizedValue || items.includes(normalizedValue)) {
+ return items;
+ }
+
+ return [...items, normalizedValue];
+};
+
+const removeValue = (items: string[], value: string): string[] => (
+ items.filter((item) => item !== value)
+);
+
+type ClaudePermissionsProps = {
+ agent: 'claude';
+ skipPermissions: boolean;
+ onSkipPermissionsChange: (value: boolean) => void;
+ allowedTools: string[];
+ onAllowedToolsChange: (value: string[]) => void;
+ disallowedTools: string[];
+ onDisallowedToolsChange: (value: string[]) => void;
+};
+
function ClaudePermissions({
skipPermissions,
- setSkipPermissions,
+ onSkipPermissionsChange,
allowedTools,
- setAllowedTools,
+ onAllowedToolsChange,
disallowedTools,
- setDisallowedTools,
- newAllowedTool,
- setNewAllowedTool,
- newDisallowedTool,
- setNewDisallowedTool,
-}) {
+ onDisallowedToolsChange,
+}: Omit) {
const { t } = useTranslation('settings');
- const addAllowedTool = (tool) => {
- if (tool && !allowedTools.includes(tool)) {
- setAllowedTools([...allowedTools, tool]);
- setNewAllowedTool('');
+ const [newAllowedTool, setNewAllowedTool] = useState('');
+ const [newDisallowedTool, setNewDisallowedTool] = useState('');
+
+ const handleAddAllowedTool = (tool: string) => {
+ const updated = addUnique(allowedTools, tool);
+ if (updated.length === allowedTools.length) {
+ return;
}
+
+ onAllowedToolsChange(updated);
+ setNewAllowedTool('');
};
- const removeAllowedTool = (tool) => {
- setAllowedTools(allowedTools.filter(t => t !== tool));
- };
-
- const addDisallowedTool = (tool) => {
- if (tool && !disallowedTools.includes(tool)) {
- setDisallowedTools([...disallowedTools, tool]);
- setNewDisallowedTool('');
+ const handleAddDisallowedTool = (tool: string) => {
+ const updated = addUnique(disallowedTools, tool);
+ if (updated.length === disallowedTools.length) {
+ return;
}
- };
- const removeDisallowedTool = (tool) => {
- setDisallowedTools(disallowedTools.filter(t => t !== tool));
+ onDisallowedToolsChange(updated);
+ setNewDisallowedTool('');
};
return (
- {/* Skip Permissions */}
-
- {t('permissions.title')}
-
+
{t('permissions.title')}
- {/* Allowed Tools */}
-
- {t('permissions.allowedTools.title')}
-
+ {t('permissions.allowedTools.title')}
-
- {t('permissions.allowedTools.description')}
-
+
{t('permissions.allowedTools.description')}
setNewAllowedTool(e.target.value)}
+ onChange={(event) => setNewAllowedTool(event.target.value)}
placeholder={t('permissions.allowedTools.placeholder')}
- onKeyPress={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- addAllowedTool(newAllowedTool);
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ handleAddAllowedTool(newAllowedTool);
}
}}
className="flex-1 h-10"
/>
- {/* Quick add buttons */}
{t('permissions.allowedTools.quickAdd')}
- {commonClaudeTools.map(tool => (
+ {COMMON_CLAUDE_TOOLS.map((tool) => (
- {allowedTools.map(tool => (
+ {allowedTools.map((tool) => (
-
- {tool}
-
+ {tool}
- {/* Disallowed Tools */}
-
- {t('permissions.blockedTools.title')}
-
+
{t('permissions.blockedTools.title')}
-
- {t('permissions.blockedTools.description')}
-
+
{t('permissions.blockedTools.description')}
setNewDisallowedTool(e.target.value)}
+ onChange={(event) => setNewDisallowedTool(event.target.value)}
placeholder={t('permissions.blockedTools.placeholder')}
- onKeyPress={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- addDisallowedTool(newDisallowedTool);
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ handleAddDisallowedTool(newDisallowedTool);
}
}}
className="flex-1 h-10"
/>
- {disallowedTools.map(tool => (
+ {disallowedTools.map((tool) => (
-
- {tool}
-
+ {tool}
- {/* Help Section */}
{t('permissions.toolExamples.title')}
@@ -260,58 +260,61 @@ function ClaudePermissions({
);
}
-// Cursor Permissions
+type CursorPermissionsProps = {
+ agent: 'cursor';
+ skipPermissions: boolean;
+ onSkipPermissionsChange: (value: boolean) => void;
+ allowedCommands: string[];
+ onAllowedCommandsChange: (value: string[]) => void;
+ disallowedCommands: string[];
+ onDisallowedCommandsChange: (value: string[]) => void;
+};
+
function CursorPermissions({
skipPermissions,
- setSkipPermissions,
+ onSkipPermissionsChange,
allowedCommands,
- setAllowedCommands,
+ onAllowedCommandsChange,
disallowedCommands,
- setDisallowedCommands,
- newAllowedCommand,
- setNewAllowedCommand,
- newDisallowedCommand,
- setNewDisallowedCommand,
-}) {
+ onDisallowedCommandsChange,
+}: Omit) {
const { t } = useTranslation('settings');
- const addAllowedCommand = (cmd) => {
- if (cmd && !allowedCommands.includes(cmd)) {
- setAllowedCommands([...allowedCommands, cmd]);
- setNewAllowedCommand('');
+ const [newAllowedCommand, setNewAllowedCommand] = useState('');
+ const [newDisallowedCommand, setNewDisallowedCommand] = useState('');
+
+ const handleAddAllowedCommand = (command: string) => {
+ const updated = addUnique(allowedCommands, command);
+ if (updated.length === allowedCommands.length) {
+ return;
}
+
+ onAllowedCommandsChange(updated);
+ setNewAllowedCommand('');
};
- const removeAllowedCommand = (cmd) => {
- setAllowedCommands(allowedCommands.filter(c => c !== cmd));
- };
-
- const addDisallowedCommand = (cmd) => {
- if (cmd && !disallowedCommands.includes(cmd)) {
- setDisallowedCommands([...disallowedCommands, cmd]);
- setNewDisallowedCommand('');
+ const handleAddDisallowedCommand = (command: string) => {
+ const updated = addUnique(disallowedCommands, command);
+ if (updated.length === disallowedCommands.length) {
+ return;
}
- };
- const removeDisallowedCommand = (cmd) => {
- setDisallowedCommands(disallowedCommands.filter(c => c !== cmd));
+ onDisallowedCommandsChange(updated);
+ setNewDisallowedCommand('');
};
return (
- {/* Skip Permissions */}
-
- {t('permissions.title')}
-
+
{t('permissions.title')}
- {/* Allowed Commands */}
-
- {t('permissions.allowedCommands.title')}
-
+ {t('permissions.allowedCommands.title')}
-
- {t('permissions.allowedCommands.description')}
-
+
{t('permissions.allowedCommands.description')}
setNewAllowedCommand(e.target.value)}
+ onChange={(event) => setNewAllowedCommand(event.target.value)}
placeholder={t('permissions.allowedCommands.placeholder')}
- onKeyPress={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- addAllowedCommand(newAllowedCommand);
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ handleAddAllowedCommand(newAllowedCommand);
}
}}
className="flex-1 h-10"
/>
- {/* Quick add buttons */}
{t('permissions.allowedCommands.quickAdd')}
- {commonCursorCommands.map(cmd => (
+ {COMMON_CURSOR_COMMANDS.map((command) => (
))}
- {allowedCommands.map(cmd => (
-
-
- {cmd}
-
+ {allowedCommands.map((command) => (
+
+ {command}
- {/* Disallowed Commands */}
-
- {t('permissions.blockedCommands.title')}
-
+
{t('permissions.blockedCommands.title')}
-
- {t('permissions.blockedCommands.description')}
-
+
{t('permissions.blockedCommands.description')}
setNewDisallowedCommand(e.target.value)}
+ onChange={(event) => setNewDisallowedCommand(event.target.value)}
placeholder={t('permissions.blockedCommands.placeholder')}
- onKeyPress={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- addDisallowedCommand(newDisallowedCommand);
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ handleAddDisallowedCommand(newDisallowedCommand);
}
}}
className="flex-1 h-10"
/>
- {disallowedCommands.map(cmd => (
-
-
- {cmd}
-
+ {disallowedCommands.map((command) => (
+
+ {command}
- {/* Help Section */}
{t('permissions.shellExamples.title')}
@@ -483,37 +470,38 @@ function CursorPermissions({
);
}
-// Codex Permissions
-function CodexPermissions({ permissionMode, setPermissionMode }) {
+type CodexPermissionsProps = {
+ agent: 'codex';
+ permissionMode: CodexPermissionMode;
+ onPermissionModeChange: (value: CodexPermissionMode) => void;
+};
+
+function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit) {
const { t } = useTranslation('settings');
+
return (
-
- {t('permissions.codex.permissionMode')}
-
+ {t('permissions.codex.permissionMode')}
-
- {t('permissions.codex.description')}
-
+
{t('permissions.codex.description')}
- {/* Default Mode */}