refactor(settings-modal): refactor settings modal to make it feature based

This commit is contained in:
Haileyesus
2026-02-18 12:52:19 +03:00
parent 7ccbc8d92d
commit 73b32a5859
16 changed files with 3015 additions and 2726 deletions

View File

@@ -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 (
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">{label}</div>
<div className="text-sm text-muted-foreground">{description}</div>
</div>
<button
onClick={() => onChange(!checked)}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
role="switch"
aria-checked={checked}
aria-label={ariaLabel}
>
<span className="sr-only">{ariaLabel}</span>
<span
className={`${
checked ? 'translate-x-7' : 'translate-x-1'
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
>
{checked ? onIcon : offIcon}
</span>
</button>
</div>
</div>
);
}
export default function AppearanceSettingsTab({
isDarkMode,
onToggleDarkMode,
projectSortOrder,
onProjectSortOrderChange,
codeEditorSettings,
onCodeEditorThemeChange,
onCodeEditorWordWrapChange,
onCodeEditorShowMinimapChange,
onCodeEditorLineNumbersChange,
onCodeEditorFontSizeChange,
}: AppearanceSettingsTabProps) {
const { t } = useTranslation('settings');
return (
<div className="space-y-6 md:space-y-8">
<div className="space-y-4">
<ToggleCard
label={t('appearanceSettings.darkMode.label')}
description={t('appearanceSettings.darkMode.description')}
checked={isDarkMode}
onChange={onToggleDarkMode}
onIcon={<Moon className="w-3.5 h-3.5 text-gray-700" />}
offIcon={<Sun className="w-3.5 h-3.5 text-yellow-500" />}
ariaLabel={t('appearanceSettings.darkMode.label')}
/>
</div>
<div className="space-y-4">
<LanguageSelector />
</div>
<div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
{t('appearanceSettings.projectSorting.label')}
</div>
<div className="text-sm text-muted-foreground">
{t('appearanceSettings.projectSorting.description')}
</div>
</div>
<select
value={projectSortOrder}
onChange={(event) => onProjectSortOrderChange(event.target.value as ProjectSortOrder)}
className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-32"
>
<option value="name">{t('appearanceSettings.projectSorting.alphabetical')}</option>
<option value="date">{t('appearanceSettings.projectSorting.recentActivity')}</option>
</select>
</div>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground">{t('appearanceSettings.codeEditor.title')}</h3>
<ToggleCard
label={t('appearanceSettings.codeEditor.theme.label')}
description={t('appearanceSettings.codeEditor.theme.description')}
checked={codeEditorSettings.theme === 'dark'}
onChange={(enabled) => onCodeEditorThemeChange(enabled ? 'dark' : 'light')}
onIcon={<Moon className="w-3.5 h-3.5 text-gray-700" />}
offIcon={<Sun className="w-3.5 h-3.5 text-yellow-500" />}
ariaLabel={t('appearanceSettings.codeEditor.theme.label')}
/>
<ToggleCard
label={t('appearanceSettings.codeEditor.wordWrap.label')}
description={t('appearanceSettings.codeEditor.wordWrap.description')}
checked={codeEditorSettings.wordWrap}
onChange={onCodeEditorWordWrapChange}
ariaLabel={t('appearanceSettings.codeEditor.wordWrap.label')}
/>
<ToggleCard
label={t('appearanceSettings.codeEditor.showMinimap.label')}
description={t('appearanceSettings.codeEditor.showMinimap.description')}
checked={codeEditorSettings.showMinimap}
onChange={onCodeEditorShowMinimapChange}
ariaLabel={t('appearanceSettings.codeEditor.showMinimap.label')}
/>
<ToggleCard
label={t('appearanceSettings.codeEditor.lineNumbers.label')}
description={t('appearanceSettings.codeEditor.lineNumbers.description')}
checked={codeEditorSettings.lineNumbers}
onChange={onCodeEditorLineNumbersChange}
ariaLabel={t('appearanceSettings.codeEditor.lineNumbers.label')}
/>
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
{t('appearanceSettings.codeEditor.fontSize.label')}
</div>
<div className="text-sm text-muted-foreground">
{t('appearanceSettings.codeEditor.fontSize.description')}
</div>
</div>
<select
value={codeEditorSettings.fontSize}
onChange={(event) => onCodeEditorFontSizeChange(event.target.value)}
className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-24"
>
<option value="10">10px</option>
<option value="11">11px</option>
<option value="12">12px</option>
<option value="13">13px</option>
<option value="14">14px</option>
<option value="15">15px</option>
<option value="16">16px</option>
<option value="18">18px</option>
<option value="20">20px</option>
</select>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,134 @@
import { LogIn } from 'lucide-react';
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';
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<AgentProvider, AgentVisualConfig> = {
claude: {
name: 'Claude',
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',
subtextClass: 'text-blue-700 dark:text-blue-300',
buttonClass: 'bg-blue-600 hover:bg-blue-700',
},
cursor: {
name: 'Cursor',
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',
subtextClass: 'text-purple-700 dark:text-purple-300',
buttonClass: 'bg-purple-600 hover:bg-purple-700',
},
codex: {
name: 'Codex',
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',
subtextClass: 'text-gray-700 dark:text-gray-300',
buttonClass: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600',
},
};
export default function AccountContent({ agent, authStatus, onLogin }: AccountContentProps) {
const { t } = useTranslation('settings');
const config = agentConfig[agent];
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-4">
<SessionProviderLogo provider={agent} className="w-6 h-6" />
<div>
<h3 className="text-lg font-medium text-foreground">{config.name}</h3>
<p className="text-sm text-muted-foreground">{t(`agents.account.${agent}.description`)}</p>
</div>
</div>
<div className={`${config.bgClass} border ${config.borderClass} rounded-lg p-4`}>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex-1">
<div className={`font-medium ${config.textClass}`}>
{t('agents.connectionStatus')}
</div>
<div className={`text-sm ${config.subtextClass}`}>
{authStatus.loading ? (
t('agents.authStatus.checkingAuth')
) : authStatus.authenticated ? (
t('agents.authStatus.loggedInAs', {
email: authStatus.email || t('agents.authStatus.authenticatedUser'),
})
) : (
t('agents.authStatus.notConnected')
)}
</div>
</div>
<div>
{authStatus.loading ? (
<Badge variant="secondary" className="bg-gray-100 dark:bg-gray-800">
{t('agents.authStatus.checking')}
</Badge>
) : authStatus.authenticated ? (
<Badge variant="secondary" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
{t('agents.authStatus.connected')}
</Badge>
) : (
<Badge variant="secondary" className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300">
{t('agents.authStatus.disconnected')}
</Badge>
)}
</div>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="flex items-center justify-between">
<div>
<div className={`font-medium ${config.textClass}`}>
{authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
</div>
<div className={`text-sm ${config.subtextClass}`}>
{authStatus.authenticated
? t('agents.login.reAuthDescription')
: t('agents.login.description', { agent: config.name })}
</div>
</div>
<Button
onClick={onLogin}
className={`${config.buttonClass} text-white`}
size="sm"
>
<LogIn className="w-4 h-4 mr-2" />
{authStatus.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
</Button>
</div>
</div>
{authStatus.error && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="text-sm text-red-600 dark:text-red-400">
{t('agents.error', { error: authStatus.error })}
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,118 @@
import { useTranslation } from 'react-i18next';
import SessionProviderLogo from '../../../../SessionProviderLogo';
import type { AgentProvider, AuthStatus } from '../../../types/types';
type AgentListItemProps = {
agentId: AgentProvider;
authStatus: AuthStatus;
isSelected: boolean;
onClick: () => void;
isMobile?: boolean;
};
type AgentConfig = {
name: string;
color: 'blue' | 'purple' | 'gray';
};
const agentConfig: Record<AgentProvider, AgentConfig> = {
claude: {
name: 'Claude',
color: 'blue',
},
cursor: {
name: 'Cursor',
color: 'purple',
},
codex: {
name: 'Codex',
color: 'gray',
},
};
const colorClasses = {
blue: {
border: 'border-l-blue-500 md:border-l-blue-500',
borderBottom: 'border-b-blue-500',
bg: 'bg-blue-50 dark:bg-blue-900/20',
dot: 'bg-blue-500',
},
purple: {
border: 'border-l-purple-500 md:border-l-purple-500',
borderBottom: 'border-b-purple-500',
bg: 'bg-purple-50 dark:bg-purple-900/20',
dot: 'bg-purple-500',
},
gray: {
border: 'border-l-gray-700 dark:border-l-gray-300',
borderBottom: 'border-b-gray-700 dark:border-b-gray-300',
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,
}: AgentListItemProps) {
const { t } = useTranslation('settings');
const config = agentConfig[agentId];
const colors = colorClasses[config.color];
if (isMobile) {
return (
<button
onClick={onClick}
className={`flex-1 text-center py-3 px-2 border-b-2 transition-colors ${
isSelected
? `${colors.borderBottom} ${colors.bg}`
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
}`}
>
<div className="flex flex-col items-center gap-1">
<SessionProviderLogo provider={agentId} className="w-5 h-5" />
<span className="text-xs font-medium text-foreground">{config.name}</span>
{authStatus.authenticated && (
<span className={`w-1.5 h-1.5 rounded-full ${colors.dot}`} />
)}
</div>
</button>
);
}
return (
<button
onClick={onClick}
className={`w-full text-left p-3 border-l-4 transition-colors ${
isSelected
? `${colors.border} ${colors.bg}`
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
}`}
>
<div className="flex items-center gap-2 mb-1">
<SessionProviderLogo provider={agentId} className="w-4 h-4" />
<span className="font-medium text-foreground">{config.name}</span>
</div>
<div className="text-xs text-muted-foreground pl-6">
{authStatus.loading ? (
<span className="text-gray-400">{t('agents.authStatus.checking')}</span>
) : authStatus.authenticated ? (
<div className="flex items-center gap-1">
<span className={`w-1.5 h-1.5 rounded-full ${colors.dot}`} />
<span className="truncate max-w-[120px]" title={authStatus.email ?? undefined}>
{authStatus.email || t('agents.authStatus.connected')}
</span>
</div>
) : (
<div className="flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
<span>{t('agents.authStatus.notConnected')}</span>
</div>
)}
</div>
</button>
);
}

View File

@@ -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<string, McpTestResult>;
mcpServerTools: Record<string, McpToolsResult>;
mcpToolsLoading: Record<string, boolean>;
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<AgentProvider>('claude');
const [selectedCategory, setSelectedCategory] = useState<AgentCategory>('account');
// Cursor MCP add/edit/delete was previously a placeholder and is intentionally preserved.
const noopCursorMcpAction = () => {};
const agentContextById = useMemo<Record<AgentProvider, AgentContext>>(() => ({
claude: {
authStatus: claudeAuthStatus,
onLogin: onClaudeLogin,
},
cursor: {
authStatus: cursorAuthStatus,
onLogin: onCursorLogin,
},
codex: {
authStatus: codexAuthStatus,
onLogin: onCodexLogin,
},
}), [
claudeAuthStatus,
codexAuthStatus,
cursorAuthStatus,
onClaudeLogin,
onCodexLogin,
onCursorLogin,
]);
return (
<div className="flex flex-col md:flex-row h-full min-h-[400px] md:min-h-[500px]">
<div className="md:hidden border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<div className="flex">
{(['claude', 'cursor', 'codex'] as AgentProvider[]).map((agent) => (
<AgentListItem
key={`mobile-${agent}`}
agentId={agent}
authStatus={agentContextById[agent].authStatus}
isSelected={selectedAgent === agent}
onClick={() => setSelectedAgent(agent)}
isMobile
/>
))}
</div>
</div>
<div className="hidden md:block w-48 border-r border-gray-200 dark:border-gray-700 flex-shrink-0">
<div className="p-2">
{(['claude', 'cursor', 'codex'] as AgentProvider[]).map((agent) => (
<AgentListItem
key={`desktop-${agent}`}
agentId={agent}
authStatus={agentContextById[agent].authStatus}
isSelected={selectedAgent === agent}
onClick={() => setSelectedAgent(agent)}
/>
))}
</div>
</div>
<div className="flex-1 flex flex-col overflow-hidden">
<div className="border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<div className="flex px-2 md:px-4 overflow-x-auto">
{(['account', 'permissions', 'mcp'] as AgentCategory[]).map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-3 md:px-4 py-2 md:py-3 text-xs md:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
selectedCategory === category
? 'border-blue-600 text-blue-600 dark:text-blue-400'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
{category === 'account' && t('tabs.account')}
{category === 'permissions' && t('tabs.permissions')}
{category === 'mcp' && t('tabs.mcpServers')}
</button>
))}
</div>
</div>
<div className="flex-1 overflow-y-auto p-3 md:p-4">
{selectedCategory === 'account' && (
<AccountContent
agent={selectedAgent}
authStatus={agentContextById[selectedAgent].authStatus}
onLogin={agentContextById[selectedAgent].onLogin}
/>
)}
{selectedCategory === 'permissions' && selectedAgent === 'claude' && (
<PermissionsContent
agent="claude"
skipPermissions={claudePermissions.skipPermissions}
onSkipPermissionsChange={(value) => {
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' && (
<PermissionsContent
agent="cursor"
skipPermissions={cursorPermissions.skipPermissions}
onSkipPermissionsChange={(value) => {
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' && (
<PermissionsContent
agent="codex"
permissionMode={codexPermissionMode}
onPermissionModeChange={onCodexPermissionModeChange}
/>
)}
{selectedCategory === 'mcp' && selectedAgent === 'claude' && (
<McpServersContent
agent="claude"
servers={mcpServers}
onAdd={() => onOpenMcpForm()}
onEdit={(server) => onOpenMcpForm(server)}
onDelete={onDeleteMcpServer}
onTest={onTestMcpServer}
onDiscoverTools={onDiscoverMcpTools}
testResults={mcpTestResults}
serverTools={mcpServerTools}
toolsLoading={mcpToolsLoading}
/>
)}
{selectedCategory === 'mcp' && selectedAgent === 'cursor' && (
<McpServersContent
agent="cursor"
servers={cursorMcpServers}
onAdd={noopCursorMcpAction}
onEdit={noopCursorMcpAction}
onDelete={noopCursorMcpAction}
/>
)}
{selectedCategory === 'mcp' && selectedAgent === 'codex' && (
<McpServersContent
agent="codex"
servers={codexMcpServers}
onAdd={() => onOpenCodexMcpForm()}
onEdit={(server) => onOpenCodexMcpForm(server)}
onDelete={(serverId) => onDeleteCodexMcpServer(serverId)}
/>
)}
</div>
</div>
</div>
);
}

View File

@@ -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 <Terminal className="w-4 h-4" />;
}
if (type === 'sse') {
return <Zap className="w-4 h-4" />;
}
if (type === 'http') {
return <Globe className="w-4 h-4" />;
}
return <Server className="w-4 h-4" />;
};
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<string, McpTestResult>;
serverTools: Record<string, McpToolsResult>;
toolsLoading: Record<string, boolean>;
};
function ClaudeMcpServers({
servers,
onAdd,
onEdit,
onDelete,
testResults,
serverTools,
}: Omit<ClaudeMcpServersProps, 'agent' | 'onTest' | 'onDiscoverTools' | 'toolsLoading'>) {
const { t } = useTranslation('settings');
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Server className="w-5 h-5 text-purple-500" />
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('mcpServers.description.claude')}</p>
<div className="flex justify-between items-center">
<Button onClick={onAdd} className="bg-purple-600 hover:bg-purple-700 text-white" size="sm">
<Plus className="w-4 h-4 mr-2" />
{t('mcpServers.addButton')}
</Button>
</div>
<div className="space-y-2">
{servers.map((server) => {
const serverId = server.id || server.name;
const testResult = testResults[serverId];
const toolsResult = serverTools[serverId];
return (
<div key={serverId} className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{getTransportIcon(server.type)}
<span className="font-medium text-foreground">{server.name}</span>
<Badge variant="outline" className="text-xs">
{server.type || 'stdio'}
</Badge>
<Badge variant="outline" className="text-xs">
{server.scope === 'local'
? t('mcpServers.scope.local')
: server.scope === 'user'
? t('mcpServers.scope.user')
: server.scope}
</Badge>
</div>
<div className="text-sm text-muted-foreground space-y-1">
{server.type === 'stdio' && server.config?.command && (
<div>
{t('mcpServers.config.command')}:{' '}
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code>
</div>
)}
{(server.type === 'sse' || server.type === 'http') && server.config?.url && (
<div>
{t('mcpServers.config.url')}:{' '}
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.url}</code>
</div>
)}
{server.config?.args && server.config.args.length > 0 && (
<div>
{t('mcpServers.config.args')}:{' '}
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.args.join(' ')}</code>
</div>
)}
</div>
{testResult && (
<div className={`mt-2 p-2 rounded text-xs ${
testResult.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}
>
<div className="font-medium">{testResult.message}</div>
</div>
)}
{toolsResult && toolsResult.tools && toolsResult.tools.length > 0 && (
<div className="mt-2 p-2 rounded text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200">
<div className="font-medium">
{t('mcpServers.tools.title')} {t('mcpServers.tools.count', { count: toolsResult.tools.length })}
</div>
<div className="flex flex-wrap gap-1 mt-1">
{toolsResult.tools.slice(0, 5).map((tool, index) => (
<code key={`${tool.name}-${index}`} className="bg-blue-100 dark:bg-blue-800 px-1 rounded">
{tool.name}
</code>
))}
{toolsResult.tools.length > 5 && (
<span className="text-xs opacity-75">
{t('mcpServers.tools.more', { count: toolsResult.tools.length - 5 })}
</span>
)}
</div>
</div>
)}
</div>
<div className="flex items-center gap-2 ml-4">
<Button
onClick={() => onEdit(server)}
variant="ghost"
size="sm"
className="text-gray-600 hover:text-gray-700"
title={t('mcpServers.actions.edit')}
>
<Edit3 className="w-4 h-4" />
</Button>
<Button
onClick={() => onDelete(serverId, server.scope)}
variant="ghost"
size="sm"
className="text-red-600 hover:text-red-700"
title={t('mcpServers.actions.delete')}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</div>
);
})}
{servers.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
)}
</div>
</div>
);
}
type CursorMcpServersProps = {
agent: 'cursor';
servers: McpServer[];
onAdd: () => void;
onEdit: (server: McpServer) => void;
onDelete: (serverId: string) => void;
};
function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpServersProps, 'agent'>) {
const { t } = useTranslation('settings');
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Server className="w-5 h-5 text-purple-500" />
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('mcpServers.description.cursor')}</p>
<div className="flex justify-between items-center">
<Button onClick={onAdd} className="bg-purple-600 hover:bg-purple-700 text-white" size="sm">
<Plus className="w-4 h-4 mr-2" />
{t('mcpServers.addButton')}
</Button>
</div>
<div className="space-y-2">
{servers.map((server) => {
const serverId = server.id || server.name;
return (
<div key={serverId} className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-4 h-4" />
<span className="font-medium text-foreground">{server.name}</span>
<Badge variant="outline" className="text-xs">stdio</Badge>
</div>
<div className="text-sm text-muted-foreground">
{server.config?.command && (
<div>
{t('mcpServers.config.command')}:{' '}
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<Button
onClick={() => onEdit(server)}
variant="ghost"
size="sm"
className="text-gray-600 hover:text-gray-700"
title={t('mcpServers.actions.edit')}
>
<Edit3 className="w-4 h-4" />
</Button>
<Button
onClick={() => onDelete(serverId)}
variant="ghost"
size="sm"
className="text-red-600 hover:text-red-700"
title={t('mcpServers.actions.delete')}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</div>
);
})}
{servers.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
)}
</div>
</div>
);
}
type CodexMcpServersProps = {
agent: 'codex';
servers: McpServer[];
onAdd: () => void;
onEdit: (server: McpServer) => void;
onDelete: (serverId: string) => void;
};
function CodexMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CodexMcpServersProps, 'agent'>) {
const { t } = useTranslation('settings');
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Server className="w-5 h-5 text-gray-700 dark:text-gray-300" />
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('mcpServers.description.codex')}</p>
<div className="flex justify-between items-center">
<Button onClick={onAdd} className="bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600 text-white" size="sm">
<Plus className="w-4 h-4 mr-2" />
{t('mcpServers.addButton')}
</Button>
</div>
<div className="space-y-2">
{servers.map((server) => (
<div key={server.name} className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-4 h-4" />
<span className="font-medium text-foreground">{server.name}</span>
<Badge variant="outline" className="text-xs">stdio</Badge>
</div>
<div className="text-sm text-muted-foreground space-y-1">
{server.config?.command && (
<div>
{t('mcpServers.config.command')}:{' '}
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code>
</div>
)}
{server.config?.args && server.config.args.length > 0 && (
<div>
{t('mcpServers.config.args')}:{' '}
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.args.join(' ')}</code>
</div>
)}
{server.config?.env && Object.keys(server.config.env).length > 0 && (
<div>
{t('mcpServers.config.environment')}:{' '}
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">
{Object.entries(server.config.env).map(([key, value]) => `${key}=${value}`).join(', ')}
</code>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<Button
onClick={() => onEdit(server)}
variant="ghost"
size="sm"
className="text-gray-600 hover:text-gray-700"
title={t('mcpServers.actions.edit')}
>
<Edit3 className="w-4 h-4" />
</Button>
<Button
onClick={() => onDelete(server.name)}
variant="ghost"
size="sm"
className="text-red-600 hover:text-red-700"
title={t('mcpServers.actions.delete')}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
{servers.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
)}
</div>
<div className="bg-gray-100 dark:bg-gray-800/50 border border-gray-300 dark:border-gray-600 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">{t('mcpServers.help.title')}</h4>
<p className="text-sm text-gray-700 dark:text-gray-300">{t('mcpServers.help.description')}</p>
</div>
</div>
);
}
type McpServersContentProps = ClaudeMcpServersProps | CursorMcpServersProps | CodexMcpServersProps;
export default function McpServersContent(props: McpServersContentProps) {
if (props.agent === 'claude') {
return <ClaudeMcpServers {...props} />;
}
if (props.agent === 'cursor') {
return <CursorMcpServers {...props} />;
}
return <CodexMcpServers {...props} />;
}

View File

@@ -0,0 +1,597 @@
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';
const COMMON_CLAUDE_TOOLS = [
'Bash(git log:*)',
'Bash(git diff:*)',
'Bash(git status:*)',
'Write',
'Read',
'Edit',
'Glob',
'Grep',
'MultiEdit',
'Task',
'TodoWrite',
'TodoRead',
'WebFetch',
'WebSearch',
];
const COMMON_CURSOR_COMMANDS = [
'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)',
];
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,
onSkipPermissionsChange,
allowedTools,
onAllowedToolsChange,
disallowedTools,
onDisallowedToolsChange,
}: Omit<ClaudePermissionsProps, 'agent'>) {
const { t } = useTranslation('settings');
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 handleAddDisallowedTool = (tool: string) => {
const updated = addUnique(disallowedTools, tool);
if (updated.length === disallowedTools.length) {
return;
}
onDisallowedToolsChange(updated);
setNewDisallowedTool('');
};
return (
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-orange-500" />
<h3 className="text-lg font-medium text-foreground">{t('permissions.title')}</h3>
</div>
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={skipPermissions}
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
/>
<div>
<div className="font-medium text-orange-900 dark:text-orange-100">
{t('permissions.skipPermissions.label')}
</div>
<div className="text-sm text-orange-700 dark:text-orange-300">
{t('permissions.skipPermissions.claudeDescription')}
</div>
</div>
</label>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-medium text-foreground">{t('permissions.allowedTools.title')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('permissions.allowedTools.description')}</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newAllowedTool}
onChange={(event) => setNewAllowedTool(event.target.value)}
placeholder={t('permissions.allowedTools.placeholder')}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleAddAllowedTool(newAllowedTool);
}
}}
className="flex-1 h-10"
/>
<Button
onClick={() => handleAddAllowedTool(newAllowedTool)}
disabled={!newAllowedTool.trim()}
size="sm"
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t('permissions.allowedTools.quickAdd')}
</p>
<div className="flex flex-wrap gap-2">
{COMMON_CLAUDE_TOOLS.map((tool) => (
<Button
key={tool}
variant="outline"
size="sm"
onClick={() => handleAddAllowedTool(tool)}
disabled={allowedTools.includes(tool)}
className="text-xs h-8"
>
{tool}
</Button>
))}
</div>
</div>
<div className="space-y-2">
{allowedTools.map((tool) => (
<div key={tool} className="flex items-center justify-between bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
<span className="font-mono text-sm text-green-800 dark:text-green-200">{tool}</span>
<Button
variant="ghost"
size="sm"
onClick={() => onAllowedToolsChange(removeValue(allowedTools, tool))}
className="text-green-600 hover:text-green-700"
>
<X className="w-4 h-4" />
</Button>
</div>
))}
{allowedTools.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
{t('permissions.allowedTools.empty')}
</div>
)}
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<h3 className="text-lg font-medium text-foreground">{t('permissions.blockedTools.title')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('permissions.blockedTools.description')}</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newDisallowedTool}
onChange={(event) => setNewDisallowedTool(event.target.value)}
placeholder={t('permissions.blockedTools.placeholder')}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleAddDisallowedTool(newDisallowedTool);
}
}}
className="flex-1 h-10"
/>
<Button
onClick={() => handleAddDisallowedTool(newDisallowedTool)}
disabled={!newDisallowedTool.trim()}
size="sm"
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
<div className="space-y-2">
{disallowedTools.map((tool) => (
<div key={tool} className="flex items-center justify-between bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<span className="font-mono text-sm text-red-800 dark:text-red-200">{tool}</span>
<Button
variant="ghost"
size="sm"
onClick={() => onDisallowedToolsChange(removeValue(disallowedTools, tool))}
className="text-red-600 hover:text-red-700"
>
<X className="w-4 h-4" />
</Button>
</div>
))}
{disallowedTools.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
{t('permissions.blockedTools.empty')}
</div>
)}
</div>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
{t('permissions.toolExamples.title')}
</h4>
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git log:*)"</code> {t('permissions.toolExamples.bashGitLog')}</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git diff:*)"</code> {t('permissions.toolExamples.bashGitDiff')}</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Write"</code> {t('permissions.toolExamples.write')}</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(rm:*)"</code> {t('permissions.toolExamples.bashRm')}</li>
</ul>
</div>
</div>
);
}
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,
onSkipPermissionsChange,
allowedCommands,
onAllowedCommandsChange,
disallowedCommands,
onDisallowedCommandsChange,
}: Omit<CursorPermissionsProps, 'agent'>) {
const { t } = useTranslation('settings');
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 handleAddDisallowedCommand = (command: string) => {
const updated = addUnique(disallowedCommands, command);
if (updated.length === disallowedCommands.length) {
return;
}
onDisallowedCommandsChange(updated);
setNewDisallowedCommand('');
};
return (
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-orange-500" />
<h3 className="text-lg font-medium text-foreground">{t('permissions.title')}</h3>
</div>
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={skipPermissions}
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
className="w-4 h-4 text-purple-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-purple-500 focus:ring-2"
/>
<div>
<div className="font-medium text-orange-900 dark:text-orange-100">
{t('permissions.skipPermissions.label')}
</div>
<div className="text-sm text-orange-700 dark:text-orange-300">
{t('permissions.skipPermissions.cursorDescription')}
</div>
</div>
</label>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-medium text-foreground">{t('permissions.allowedCommands.title')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('permissions.allowedCommands.description')}</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newAllowedCommand}
onChange={(event) => setNewAllowedCommand(event.target.value)}
placeholder={t('permissions.allowedCommands.placeholder')}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleAddAllowedCommand(newAllowedCommand);
}
}}
className="flex-1 h-10"
/>
<Button
onClick={() => handleAddAllowedCommand(newAllowedCommand)}
disabled={!newAllowedCommand.trim()}
size="sm"
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t('permissions.allowedCommands.quickAdd')}
</p>
<div className="flex flex-wrap gap-2">
{COMMON_CURSOR_COMMANDS.map((command) => (
<Button
key={command}
variant="outline"
size="sm"
onClick={() => handleAddAllowedCommand(command)}
disabled={allowedCommands.includes(command)}
className="text-xs h-8"
>
{command}
</Button>
))}
</div>
</div>
<div className="space-y-2">
{allowedCommands.map((command) => (
<div key={command} className="flex items-center justify-between bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
<span className="font-mono text-sm text-green-800 dark:text-green-200">{command}</span>
<Button
variant="ghost"
size="sm"
onClick={() => onAllowedCommandsChange(removeValue(allowedCommands, command))}
className="text-green-600 hover:text-green-700"
>
<X className="w-4 h-4" />
</Button>
</div>
))}
{allowedCommands.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
{t('permissions.allowedCommands.empty')}
</div>
)}
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<h3 className="text-lg font-medium text-foreground">{t('permissions.blockedCommands.title')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('permissions.blockedCommands.description')}</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newDisallowedCommand}
onChange={(event) => setNewDisallowedCommand(event.target.value)}
placeholder={t('permissions.blockedCommands.placeholder')}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleAddDisallowedCommand(newDisallowedCommand);
}
}}
className="flex-1 h-10"
/>
<Button
onClick={() => handleAddDisallowedCommand(newDisallowedCommand)}
disabled={!newDisallowedCommand.trim()}
size="sm"
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
<div className="space-y-2">
{disallowedCommands.map((command) => (
<div key={command} className="flex items-center justify-between bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<span className="font-mono text-sm text-red-800 dark:text-red-200">{command}</span>
<Button
variant="ghost"
size="sm"
onClick={() => onDisallowedCommandsChange(removeValue(disallowedCommands, command))}
className="text-red-600 hover:text-red-700"
>
<X className="w-4 h-4" />
</Button>
</div>
))}
{disallowedCommands.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
{t('permissions.blockedCommands.empty')}
</div>
)}
</div>
</div>
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
<h4 className="font-medium text-purple-900 dark:text-purple-100 mb-2">
{t('permissions.shellExamples.title')}
</h4>
<ul className="text-sm text-purple-800 dark:text-purple-200 space-y-1">
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(ls)"</code> {t('permissions.shellExamples.ls')}</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(git status)"</code> {t('permissions.shellExamples.gitStatus')}</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(npm install)"</code> {t('permissions.shellExamples.npmInstall')}</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(rm -rf)"</code> {t('permissions.shellExamples.rmRf')}</li>
</ul>
</div>
</div>
);
}
type CodexPermissionsProps = {
agent: 'codex';
permissionMode: CodexPermissionMode;
onPermissionModeChange: (value: CodexPermissionMode) => void;
};
function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<CodexPermissionsProps, 'agent'>) {
const { t } = useTranslation('settings');
return (
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-medium text-foreground">{t('permissions.codex.permissionMode')}</h3>
</div>
<p className="text-sm text-muted-foreground">{t('permissions.codex.description')}</p>
<div
className={`border rounded-lg p-4 cursor-pointer transition-all ${
permissionMode === 'default'
? 'bg-gray-100 dark:bg-gray-800 border-gray-400 dark:border-gray-500'
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
onClick={() => onPermissionModeChange('default')}
>
<label className="flex items-start gap-3 cursor-pointer">
<input
type="radio"
name="codexPermissionMode"
checked={permissionMode === 'default'}
onChange={() => onPermissionModeChange('default')}
className="mt-1 w-4 h-4 text-green-600"
/>
<div>
<div className="font-medium text-foreground">{t('permissions.codex.modes.default.title')}</div>
<div className="text-sm text-muted-foreground">
{t('permissions.codex.modes.default.description')}
</div>
</div>
</label>
</div>
<div
className={`border rounded-lg p-4 cursor-pointer transition-all ${
permissionMode === 'acceptEdits'
? 'bg-green-50 dark:bg-green-900/20 border-green-400 dark:border-green-600'
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
onClick={() => onPermissionModeChange('acceptEdits')}
>
<label className="flex items-start gap-3 cursor-pointer">
<input
type="radio"
name="codexPermissionMode"
checked={permissionMode === 'acceptEdits'}
onChange={() => onPermissionModeChange('acceptEdits')}
className="mt-1 w-4 h-4 text-green-600"
/>
<div>
<div className="font-medium text-green-900 dark:text-green-100">{t('permissions.codex.modes.acceptEdits.title')}</div>
<div className="text-sm text-green-700 dark:text-green-300">
{t('permissions.codex.modes.acceptEdits.description')}
</div>
</div>
</label>
</div>
<div
className={`border rounded-lg p-4 cursor-pointer transition-all ${
permissionMode === 'bypassPermissions'
? 'bg-orange-50 dark:bg-orange-900/20 border-orange-400 dark:border-orange-600'
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
onClick={() => onPermissionModeChange('bypassPermissions')}
>
<label className="flex items-start gap-3 cursor-pointer">
<input
type="radio"
name="codexPermissionMode"
checked={permissionMode === 'bypassPermissions'}
onChange={() => onPermissionModeChange('bypassPermissions')}
className="mt-1 w-4 h-4 text-orange-600"
/>
<div>
<div className="font-medium text-orange-900 dark:text-orange-100 flex items-center gap-2">
{t('permissions.codex.modes.bypassPermissions.title')}
<AlertTriangle className="w-4 h-4" />
</div>
<div className="text-sm text-orange-700 dark:text-orange-300">
{t('permissions.codex.modes.bypassPermissions.description')}
</div>
</div>
</label>
</div>
<details className="text-sm">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
{t('permissions.codex.technicalDetails')}
</summary>
<div className="mt-2 p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg text-xs text-muted-foreground space-y-2">
<p><strong>{t('permissions.codex.modes.default.title')}:</strong> {t('permissions.codex.technicalInfo.default')}</p>
<p><strong>{t('permissions.codex.modes.acceptEdits.title')}:</strong> {t('permissions.codex.technicalInfo.acceptEdits')}</p>
<p><strong>{t('permissions.codex.modes.bypassPermissions.title')}:</strong> {t('permissions.codex.technicalInfo.bypassPermissions')}</p>
<p className="text-xs opacity-75">{t('permissions.codex.technicalInfo.overrideNote')}</p>
</div>
</details>
</div>
</div>
);
}
type PermissionsContentProps = ClaudePermissionsProps | CursorPermissionsProps | CodexPermissionsProps;
export default function PermissionsContent(props: PermissionsContentProps) {
if (props.agent === 'claude') {
return <ClaudePermissions {...props} />;
}
if (props.agent === 'cursor') {
return <CursorPermissions {...props} />;
}
return <CodexPermissions {...props} />;
}