diff --git a/src/components/CredentialsSettings.jsx b/src/components/CredentialsSettings.jsx
index cc7d424..b56584a 100644
--- a/src/components/CredentialsSettings.jsx
+++ b/src/components/CredentialsSettings.jsx
@@ -1,421 +1,3 @@
-import { useState, useEffect } from 'react';
-import { Button } from './ui/button';
-import { Input } from './ui/input';
-import { Key, Plus, Trash2, Eye, EyeOff, Copy, Check, Github, ExternalLink } from 'lucide-react';
-import { useVersionCheck } from '../hooks/useVersionCheck';
-import { version } from '../../package.json';
-import { authenticatedFetch } from '../utils/api';
-import { useTranslation } from 'react-i18next';
+import CredentialsSettingsTab from './settings/view/tabs/api-settings/CredentialsSettingsTab';
-function CredentialsSettings() {
- const { t } = useTranslation('settings');
- const [apiKeys, setApiKeys] = useState([]);
- const [githubCredentials, setGithubCredentials] = useState([]);
- const [loading, setLoading] = useState(true);
- const [showNewKeyForm, setShowNewKeyForm] = useState(false);
- const [showNewGithubForm, setShowNewGithubForm] = useState(false);
- const [newKeyName, setNewKeyName] = useState('');
- const [newGithubName, setNewGithubName] = useState('');
- const [newGithubToken, setNewGithubToken] = useState('');
- const [newGithubDescription, setNewGithubDescription] = useState('');
- const [showToken, setShowToken] = useState({});
- const [copiedKey, setCopiedKey] = useState(null);
- const [newlyCreatedKey, setNewlyCreatedKey] = useState(null);
-
- // Version check hook
- const { updateAvailable, latestVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
-
- useEffect(() => {
- fetchData();
- }, []);
-
- const fetchData = async () => {
- try {
- setLoading(true);
-
- // Fetch API keys
- const apiKeysRes = await authenticatedFetch('/api/settings/api-keys');
- const apiKeysData = await apiKeysRes.json();
- setApiKeys(apiKeysData.apiKeys || []);
-
- // Fetch GitHub credentials only
- const credentialsRes = await authenticatedFetch('/api/settings/credentials?type=github_token');
- const credentialsData = await credentialsRes.json();
- setGithubCredentials(credentialsData.credentials || []);
- } catch (error) {
- console.error('Error fetching settings:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const createApiKey = async () => {
- if (!newKeyName.trim()) return;
-
- try {
- const res = await authenticatedFetch('/api/settings/api-keys', {
- method: 'POST',
- body: JSON.stringify({ keyName: newKeyName })
- });
-
- const data = await res.json();
- if (data.success) {
- setNewlyCreatedKey(data.apiKey);
- setNewKeyName('');
- setShowNewKeyForm(false);
- fetchData();
- }
- } catch (error) {
- console.error('Error creating API key:', error);
- }
- };
-
- const deleteApiKey = async (keyId) => {
- if (!confirm(t('apiKeys.confirmDelete'))) return;
-
- try {
- await authenticatedFetch(`/api/settings/api-keys/${keyId}`, {
- method: 'DELETE'
- });
- fetchData();
- } catch (error) {
- console.error('Error deleting API key:', error);
- }
- };
-
- const toggleApiKey = async (keyId, isActive) => {
- try {
- await authenticatedFetch(`/api/settings/api-keys/${keyId}/toggle`, {
- method: 'PATCH',
- body: JSON.stringify({ isActive: !isActive })
- });
- fetchData();
- } catch (error) {
- console.error('Error toggling API key:', error);
- }
- };
-
- const createGithubCredential = async () => {
- if (!newGithubName.trim() || !newGithubToken.trim()) return;
-
- try {
- const res = await authenticatedFetch('/api/settings/credentials', {
- method: 'POST',
- body: JSON.stringify({
- credentialName: newGithubName,
- credentialType: 'github_token',
- credentialValue: newGithubToken,
- description: newGithubDescription
- })
- });
-
- const data = await res.json();
- if (data.success) {
- setNewGithubName('');
- setNewGithubToken('');
- setNewGithubDescription('');
- setShowNewGithubForm(false);
- fetchData();
- }
- } catch (error) {
- console.error('Error creating GitHub credential:', error);
- }
- };
-
- const deleteGithubCredential = async (credentialId) => {
- if (!confirm(t('apiKeys.github.confirmDelete'))) return;
-
- try {
- await authenticatedFetch(`/api/settings/credentials/${credentialId}`, {
- method: 'DELETE'
- });
- fetchData();
- } catch (error) {
- console.error('Error deleting GitHub credential:', error);
- }
- };
-
- const toggleGithubCredential = async (credentialId, isActive) => {
- try {
- await authenticatedFetch(`/api/settings/credentials/${credentialId}/toggle`, {
- method: 'PATCH',
- body: JSON.stringify({ isActive: !isActive })
- });
- fetchData();
- } catch (error) {
- console.error('Error toggling GitHub credential:', error);
- }
- };
-
- const copyToClipboard = (text, id) => {
- navigator.clipboard.writeText(text);
- setCopiedKey(id);
- setTimeout(() => setCopiedKey(null), 2000);
- };
-
- if (loading) {
- return
{t('apiKeys.loading')}
;
- }
-
- return (
-
- {/* New API Key Alert */}
- {newlyCreatedKey && (
-
-
{t('apiKeys.newKey.alertTitle')}
-
- {t('apiKeys.newKey.alertMessage')}
-
-
-
- {newlyCreatedKey.apiKey}
-
-
-
-
-
- )}
-
- {/* API Keys Section */}
-
-
-
-
-
{t('apiKeys.title')}
-
-
-
-
-
-
- {showNewKeyForm && (
-
-
setNewKeyName(e.target.value)}
- className="mb-2"
- />
-
-
-
-
-
- )}
-
-
- {apiKeys.length === 0 ? (
-
{t('apiKeys.empty')}
- ) : (
- apiKeys.map((key) => (
-
-
-
{key.key_name}
-
{key.api_key}
-
- {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()}
- {key.last_used && ` • ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}`}
-
-
-
-
-
-
-
- ))
- )}
-
-
-
- {/* GitHub Credentials Section */}
-
-
-
-
-
{t('apiKeys.github.title')}
-
-
-
-
-
- {t('apiKeys.github.descriptionAlt')}
-
-
- {showNewGithubForm && (
-
- )}
-
-
- {githubCredentials.length === 0 ? (
-
{t('apiKeys.github.empty')}
- ) : (
- githubCredentials.map((credential) => (
-
-
-
{credential.credential_name}
- {credential.description && (
-
{credential.description}
- )}
-
- {t('apiKeys.github.added')} {new Date(credential.created_at).toLocaleDateString()}
-
-
-
-
-
-
-
- ))
- )}
-
-
-
- {/* Version Information */}
-
-
- );
-}
-
-export default CredentialsSettings;
+export default CredentialsSettingsTab;
diff --git a/src/components/GitSettings.jsx b/src/components/GitSettings.jsx
index cdce8e4..eaa6598 100644
--- a/src/components/GitSettings.jsx
+++ b/src/components/GitSettings.jsx
@@ -1,131 +1,3 @@
-import { useState, useEffect } from 'react';
-import { Button } from './ui/button';
-import { Input } from './ui/input';
-import { GitBranch, Check } from 'lucide-react';
-import { authenticatedFetch } from '../utils/api';
-import { useTranslation } from 'react-i18next';
+import GitSettingsTab from './settings/view/tabs/git-settings/GitSettingsTab';
-function GitSettings() {
- const { t } = useTranslation('settings');
- const [gitName, setGitName] = useState('');
- const [gitEmail, setGitEmail] = useState('');
- const [gitConfigLoading, setGitConfigLoading] = useState(false);
- const [gitConfigSaving, setGitConfigSaving] = useState(false);
- const [saveStatus, setSaveStatus] = useState(null);
-
- useEffect(() => {
- loadGitConfig();
- }, []);
-
- const loadGitConfig = async () => {
- try {
- setGitConfigLoading(true);
- const response = await authenticatedFetch('/api/user/git-config');
- if (response.ok) {
- const data = await response.json();
- setGitName(data.gitName || '');
- setGitEmail(data.gitEmail || '');
- }
- } catch (error) {
- console.error('Error loading git config:', error);
- } finally {
- setGitConfigLoading(false);
- }
- };
-
- const saveGitConfig = async () => {
- try {
- setGitConfigSaving(true);
- const response = await authenticatedFetch('/api/user/git-config', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ gitName, gitEmail })
- });
-
- if (response.ok) {
- setSaveStatus('success');
- setTimeout(() => setSaveStatus(null), 3000);
- } else {
- const data = await response.json();
- setSaveStatus('error');
- console.error('Failed to save git config:', data.error);
- }
- } catch (error) {
- console.error('Error saving git config:', error);
- setSaveStatus('error');
- } finally {
- setGitConfigSaving(false);
- }
- };
-
- return (
-
-
-
-
-
{t('git.title')}
-
-
-
- {t('git.description')}
-
-
-
-
-
-
setGitName(e.target.value)}
- placeholder="John Doe"
- disabled={gitConfigLoading}
- className="w-full"
- />
-
- {t('git.name.help')}
-
-
-
-
-
-
setGitEmail(e.target.value)}
- placeholder="john@example.com"
- disabled={gitConfigLoading}
- className="w-full"
- />
-
- {t('git.email.help')}
-
-
-
-
-
-
- {saveStatus === 'success' && (
-
-
- {t('git.status.success')}
-
- )}
-
-
-
-
- );
-}
-
-export default GitSettings;
+export default GitSettingsTab;
diff --git a/src/components/TasksSettings.jsx b/src/components/TasksSettings.jsx
index 99f35c3..e341115 100644
--- a/src/components/TasksSettings.jsx
+++ b/src/components/TasksSettings.jsx
@@ -1,109 +1,3 @@
-import { Zap } from 'lucide-react';
-import { useTasksSettings } from '../contexts/TasksSettingsContext';
-import { useTranslation } from 'react-i18next';
+import TasksSettingsTab from './settings/view/tabs/tasks-settings/TasksSettingsTab';
-function TasksSettings() {
- const { t } = useTranslation('settings');
- const {
- tasksEnabled,
- setTasksEnabled,
- isTaskMasterInstalled,
- isCheckingInstallation
- } = useTasksSettings();
-
- return (
-
- {/* Installation Status Check */}
- {isCheckingInstallation ? (
-
-
-
-
{t('tasks.checking')}
-
-
- ) : (
- <>
- {/* TaskMaster Not Installed Warning */}
- {!isTaskMasterInstalled && (
-
-
-
-
-
- {t('tasks.notInstalled.title')}
-
-
-
{t('tasks.notInstalled.description')}
-
-
- {t('tasks.notInstalled.installCommand')}
-
-
-
-
-
-
{t('tasks.notInstalled.afterInstallation')}
-
- - {t('tasks.notInstalled.steps.restart')}
- - {t('tasks.notInstalled.steps.autoAvailable')}
- - {t('tasks.notInstalled.steps.initCommand')}
-
-
-
-
-
-
- )}
-
- {/* TaskMaster Settings */}
- {isTaskMasterInstalled && (
-
-
-
-
-
- {t('tasks.settings.enableLabel')}
-
-
- {t('tasks.settings.enableDescription')}
-
-
-
-
-
-
- )}
- >
- )}
-
- );
-}
-
-export default TasksSettings;
+export default TasksSettingsTab;
diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx
index fb6735d..f596e03 100644
--- a/src/components/settings/Settings.tsx
+++ b/src/components/settings/Settings.tsx
@@ -1,15 +1,15 @@
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 CredentialsSettingsTab from './view/tabs/api-settings/CredentialsSettingsTab';
+import GitSettingsTab from './view/tabs/git-settings/GitSettingsTab';
+import TasksSettingsTab from './view/tabs/tasks-settings/TasksSettingsTab';
import { useSettingsController } from './hooks/useSettingsController';
import type { AgentProvider, SettingsProject, SettingsProps } from './types/types';
@@ -129,7 +129,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
/>
)}
- {activeTab === 'git' && }
+ {activeTab === 'git' && }
{activeTab === 'agents' && (
-
+
)}
{activeTab === 'api' && (
-
+
)}
diff --git a/src/components/settings/hooks/useCredentialsSettings.ts b/src/components/settings/hooks/useCredentialsSettings.ts
new file mode 100644
index 0000000..941ea65
--- /dev/null
+++ b/src/components/settings/hooks/useCredentialsSettings.ts
@@ -0,0 +1,272 @@
+import { useCallback, useEffect, useState } from 'react';
+import { authenticatedFetch } from '../../../utils/api';
+import type {
+ ApiKeyItem,
+ ApiKeysResponse,
+ CreatedApiKey,
+ GithubCredentialItem,
+ GithubCredentialsResponse,
+} from '../view/tabs/api-settings/types';
+
+type UseCredentialsSettingsArgs = {
+ confirmDeleteApiKeyText: string;
+ confirmDeleteGithubCredentialText: string;
+};
+
+const getApiError = (payload: { error?: string } | undefined, fallback: string) => (
+ payload?.error || fallback
+);
+
+export function useCredentialsSettings({
+ confirmDeleteApiKeyText,
+ confirmDeleteGithubCredentialText,
+}: UseCredentialsSettingsArgs) {
+ const [apiKeys, setApiKeys] = useState([]);
+ const [githubCredentials, setGithubCredentials] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const [showNewKeyForm, setShowNewKeyForm] = useState(false);
+ const [newKeyName, setNewKeyName] = useState('');
+
+ const [showNewGithubForm, setShowNewGithubForm] = useState(false);
+ const [newGithubName, setNewGithubName] = useState('');
+ const [newGithubToken, setNewGithubToken] = useState('');
+ const [newGithubDescription, setNewGithubDescription] = useState('');
+
+ const [showToken, setShowToken] = useState>({});
+ const [copiedKey, setCopiedKey] = useState(null);
+ const [newlyCreatedKey, setNewlyCreatedKey] = useState(null);
+
+ const fetchData = useCallback(async () => {
+ try {
+ setLoading(true);
+
+ const [apiKeysResponse, credentialsResponse] = await Promise.all([
+ authenticatedFetch('/api/settings/api-keys'),
+ authenticatedFetch('/api/settings/credentials?type=github_token'),
+ ]);
+
+ const [apiKeysPayload, credentialsPayload] = await Promise.all([
+ apiKeysResponse.json() as Promise,
+ credentialsResponse.json() as Promise,
+ ]);
+
+ setApiKeys(apiKeysPayload.apiKeys || []);
+ setGithubCredentials(credentialsPayload.credentials || []);
+ } catch (error) {
+ console.error('Error fetching settings:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const createApiKey = useCallback(async () => {
+ if (!newKeyName.trim()) {
+ return;
+ }
+
+ try {
+ const response = await authenticatedFetch('/api/settings/api-keys', {
+ method: 'POST',
+ body: JSON.stringify({ keyName: newKeyName.trim() }),
+ });
+
+ const payload = await response.json() as ApiKeysResponse;
+ if (!response.ok || !payload.success) {
+ console.error('Error creating API key:', getApiError(payload, 'Failed to create API key'));
+ return;
+ }
+
+ if (payload.apiKey) {
+ setNewlyCreatedKey(payload.apiKey);
+ }
+ setNewKeyName('');
+ setShowNewKeyForm(false);
+ await fetchData();
+ } catch (error) {
+ console.error('Error creating API key:', error);
+ }
+ }, [fetchData, newKeyName]);
+
+ const deleteApiKey = useCallback(async (keyId: string) => {
+ if (!window.confirm(confirmDeleteApiKeyText)) {
+ return;
+ }
+
+ try {
+ const response = await authenticatedFetch(`/api/settings/api-keys/${keyId}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ const payload = await response.json() as ApiKeysResponse;
+ console.error('Error deleting API key:', getApiError(payload, 'Failed to delete API key'));
+ return;
+ }
+
+ await fetchData();
+ } catch (error) {
+ console.error('Error deleting API key:', error);
+ }
+ }, [confirmDeleteApiKeyText, fetchData]);
+
+ const toggleApiKey = useCallback(async (keyId: string, isActive: boolean) => {
+ try {
+ const response = await authenticatedFetch(`/api/settings/api-keys/${keyId}/toggle`, {
+ method: 'PATCH',
+ body: JSON.stringify({ isActive: !isActive }),
+ });
+
+ if (!response.ok) {
+ const payload = await response.json() as ApiKeysResponse;
+ console.error('Error toggling API key:', getApiError(payload, 'Failed to toggle API key'));
+ return;
+ }
+
+ await fetchData();
+ } catch (error) {
+ console.error('Error toggling API key:', error);
+ }
+ }, [fetchData]);
+
+ const createGithubCredential = useCallback(async () => {
+ if (!newGithubName.trim() || !newGithubToken.trim()) {
+ return;
+ }
+
+ try {
+ const response = await authenticatedFetch('/api/settings/credentials', {
+ method: 'POST',
+ body: JSON.stringify({
+ credentialName: newGithubName.trim(),
+ credentialType: 'github_token',
+ credentialValue: newGithubToken,
+ description: newGithubDescription.trim(),
+ }),
+ });
+
+ const payload = await response.json() as GithubCredentialsResponse;
+ if (!response.ok || !payload.success) {
+ console.error('Error creating GitHub credential:', getApiError(payload, 'Failed to create GitHub credential'));
+ return;
+ }
+
+ setNewGithubName('');
+ setNewGithubToken('');
+ setNewGithubDescription('');
+ setShowNewGithubForm(false);
+ setShowToken((prev) => ({ ...prev, new: false }));
+ await fetchData();
+ } catch (error) {
+ console.error('Error creating GitHub credential:', error);
+ }
+ }, [fetchData, newGithubDescription, newGithubName, newGithubToken]);
+
+ const deleteGithubCredential = useCallback(async (credentialId: string) => {
+ if (!window.confirm(confirmDeleteGithubCredentialText)) {
+ return;
+ }
+
+ try {
+ const response = await authenticatedFetch(`/api/settings/credentials/${credentialId}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ const payload = await response.json() as GithubCredentialsResponse;
+ console.error('Error deleting GitHub credential:', getApiError(payload, 'Failed to delete GitHub credential'));
+ return;
+ }
+
+ await fetchData();
+ } catch (error) {
+ console.error('Error deleting GitHub credential:', error);
+ }
+ }, [confirmDeleteGithubCredentialText, fetchData]);
+
+ const toggleGithubCredential = useCallback(async (credentialId: string, isActive: boolean) => {
+ try {
+ const response = await authenticatedFetch(`/api/settings/credentials/${credentialId}/toggle`, {
+ method: 'PATCH',
+ body: JSON.stringify({ isActive: !isActive }),
+ });
+
+ if (!response.ok) {
+ const payload = await response.json() as GithubCredentialsResponse;
+ console.error('Error toggling GitHub credential:', getApiError(payload, 'Failed to toggle GitHub credential'));
+ return;
+ }
+
+ await fetchData();
+ } catch (error) {
+ console.error('Error toggling GitHub credential:', error);
+ }
+ }, [fetchData]);
+
+ const copyToClipboard = useCallback(async (text: string, id: string) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopiedKey(id);
+ window.setTimeout(() => setCopiedKey(null), 2000);
+ } catch (error) {
+ console.error('Failed to copy to clipboard:', error);
+ }
+ }, []);
+
+ const dismissNewlyCreatedKey = useCallback(() => {
+ setNewlyCreatedKey(null);
+ }, []);
+
+ const cancelNewApiKeyForm = useCallback(() => {
+ setShowNewKeyForm(false);
+ setNewKeyName('');
+ }, []);
+
+ const cancelNewGithubForm = useCallback(() => {
+ setShowNewGithubForm(false);
+ setNewGithubName('');
+ setNewGithubToken('');
+ setNewGithubDescription('');
+ setShowToken((prev) => ({ ...prev, new: false }));
+ }, []);
+
+ const toggleNewGithubTokenVisibility = useCallback(() => {
+ setShowToken((prev) => ({ ...prev, new: !prev.new }));
+ }, []);
+
+ useEffect(() => {
+ void fetchData();
+ }, [fetchData]);
+
+ return {
+ apiKeys,
+ githubCredentials,
+ loading,
+ showNewKeyForm,
+ setShowNewKeyForm,
+ newKeyName,
+ setNewKeyName,
+ showNewGithubForm,
+ setShowNewGithubForm,
+ newGithubName,
+ setNewGithubName,
+ newGithubToken,
+ setNewGithubToken,
+ newGithubDescription,
+ setNewGithubDescription,
+ showToken,
+ copiedKey,
+ newlyCreatedKey,
+ createApiKey,
+ deleteApiKey,
+ toggleApiKey,
+ createGithubCredential,
+ deleteGithubCredential,
+ toggleGithubCredential,
+ copyToClipboard,
+ dismissNewlyCreatedKey,
+ cancelNewApiKeyForm,
+ cancelNewGithubForm,
+ toggleNewGithubTokenVisibility,
+ };
+}
diff --git a/src/components/settings/hooks/useGitSettings.ts b/src/components/settings/hooks/useGitSettings.ts
new file mode 100644
index 0000000..23392b0
--- /dev/null
+++ b/src/components/settings/hooks/useGitSettings.ts
@@ -0,0 +1,96 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { authenticatedFetch } from '../../../utils/api';
+
+type GitConfigResponse = {
+ gitName?: string;
+ gitEmail?: string;
+ error?: string;
+};
+
+type SaveStatus = 'success' | 'error' | null;
+
+export function useGitSettings() {
+ const [gitName, setGitName] = useState('');
+ const [gitEmail, setGitEmail] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveStatus, setSaveStatus] = useState(null);
+ const clearStatusTimerRef = useRef(null);
+
+ const clearSaveStatus = useCallback(() => {
+ if (clearStatusTimerRef.current !== null) {
+ window.clearTimeout(clearStatusTimerRef.current);
+ clearStatusTimerRef.current = null;
+ }
+ setSaveStatus(null);
+ }, []);
+
+ const loadGitConfig = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const response = await authenticatedFetch('/api/user/git-config');
+ if (!response.ok) {
+ return;
+ }
+
+ const data = await response.json() as GitConfigResponse;
+ setGitName(data.gitName || '');
+ setGitEmail(data.gitEmail || '');
+ } catch (error) {
+ console.error('Error loading git config:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const saveGitConfig = useCallback(async () => {
+ try {
+ setIsSaving(true);
+ const response = await authenticatedFetch('/api/user/git-config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ gitName, gitEmail }),
+ });
+
+ if (response.ok) {
+ setSaveStatus('success');
+ clearStatusTimerRef.current = window.setTimeout(() => {
+ setSaveStatus(null);
+ clearStatusTimerRef.current = null;
+ }, 3000);
+ return;
+ }
+
+ const data = await response.json() as GitConfigResponse;
+ console.error('Failed to save git config:', data.error);
+ setSaveStatus('error');
+ } catch (error) {
+ console.error('Error saving git config:', error);
+ setSaveStatus('error');
+ } finally {
+ setIsSaving(false);
+ }
+ }, [gitEmail, gitName]);
+
+ useEffect(() => {
+ void loadGitConfig();
+ }, [loadGitConfig]);
+
+ useEffect(() => () => {
+ if (clearStatusTimerRef.current !== null) {
+ window.clearTimeout(clearStatusTimerRef.current);
+ }
+ }, []);
+
+ return {
+ gitName,
+ setGitName,
+ gitEmail,
+ setGitEmail,
+ isLoading,
+ isSaving,
+ saveStatus,
+ clearSaveStatus,
+ saveGitConfig,
+ };
+}
diff --git a/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx b/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx
new file mode 100644
index 0000000..0f47512
--- /dev/null
+++ b/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx
@@ -0,0 +1,100 @@
+import { useTranslation } from 'react-i18next';
+import { useVersionCheck } from '../../../../../hooks/useVersionCheck';
+import { useCredentialsSettings } from '../../../hooks/useCredentialsSettings';
+import ApiKeysSection from './sections/ApiKeysSection';
+import GithubCredentialsSection from './sections/GithubCredentialsSection';
+import NewApiKeyAlert from './sections/NewApiKeyAlert';
+import VersionInfoSection from './sections/VersionInfoSection';
+
+export default function CredentialsSettingsTab() {
+ const { t } = useTranslation('settings');
+ const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
+ const {
+ apiKeys,
+ githubCredentials,
+ loading,
+ showNewKeyForm,
+ setShowNewKeyForm,
+ newKeyName,
+ setNewKeyName,
+ showNewGithubForm,
+ setShowNewGithubForm,
+ newGithubName,
+ setNewGithubName,
+ newGithubToken,
+ setNewGithubToken,
+ newGithubDescription,
+ setNewGithubDescription,
+ showToken,
+ copiedKey,
+ newlyCreatedKey,
+ createApiKey,
+ deleteApiKey,
+ toggleApiKey,
+ createGithubCredential,
+ deleteGithubCredential,
+ toggleGithubCredential,
+ copyToClipboard,
+ dismissNewlyCreatedKey,
+ cancelNewApiKeyForm,
+ cancelNewGithubForm,
+ toggleNewGithubTokenVisibility,
+ } = useCredentialsSettings({
+ confirmDeleteApiKeyText: t('apiKeys.confirmDelete'),
+ confirmDeleteGithubCredentialText: t('apiKeys.github.confirmDelete'),
+ });
+
+ if (loading) {
+ return {t('apiKeys.loading')}
;
+ }
+
+ return (
+
+ {newlyCreatedKey && (
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsx b/src/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsx
new file mode 100644
index 0000000..200d9c9
--- /dev/null
+++ b/src/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsx
@@ -0,0 +1,109 @@
+import { ExternalLink, Key, Plus, Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Button } from '../../../../../ui/button';
+import { Input } from '../../../../../ui/input';
+import type { ApiKeyItem } from '../types';
+
+type ApiKeysSectionProps = {
+ apiKeys: ApiKeyItem[];
+ showNewKeyForm: boolean;
+ newKeyName: string;
+ onShowNewKeyFormChange: (value: boolean) => void;
+ onNewKeyNameChange: (value: string) => void;
+ onCreateApiKey: () => void;
+ onCancelCreateApiKey: () => void;
+ onToggleApiKey: (keyId: string, isActive: boolean) => void;
+ onDeleteApiKey: (keyId: string) => void;
+};
+
+export default function ApiKeysSection({
+ apiKeys,
+ showNewKeyForm,
+ newKeyName,
+ onShowNewKeyFormChange,
+ onNewKeyNameChange,
+ onCreateApiKey,
+ onCancelCreateApiKey,
+ onToggleApiKey,
+ onDeleteApiKey,
+}: ApiKeysSectionProps) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+
+
+
{t('apiKeys.title')}
+
+
+
+
+
+
+ {showNewKeyForm && (
+
+
onNewKeyNameChange(event.target.value)}
+ className="mb-2"
+ />
+
+
+
+
+
+ )}
+
+
+ {apiKeys.length === 0 ? (
+
{t('apiKeys.empty')}
+ ) : (
+ apiKeys.map((key) => (
+
+
+
{key.key_name}
+
{key.api_key}
+
+ {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()}
+ {key.last_used
+ ? ` - ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}`
+ : ''}
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+ );
+}
diff --git a/src/components/settings/view/tabs/api-settings/sections/GithubCredentialsSection.tsx b/src/components/settings/view/tabs/api-settings/sections/GithubCredentialsSection.tsx
new file mode 100644
index 0000000..6fe1403
--- /dev/null
+++ b/src/components/settings/view/tabs/api-settings/sections/GithubCredentialsSection.tsx
@@ -0,0 +1,141 @@
+import { Eye, EyeOff, Github, Plus, Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Button } from '../../../../../ui/button';
+import { Input } from '../../../../../ui/input';
+import type { GithubCredentialItem } from '../types';
+
+type GithubCredentialsSectionProps = {
+ githubCredentials: GithubCredentialItem[];
+ showNewGithubForm: boolean;
+ showNewTokenPlainText: boolean;
+ newGithubName: string;
+ newGithubToken: string;
+ newGithubDescription: string;
+ onShowNewGithubFormChange: (value: boolean) => void;
+ onNewGithubNameChange: (value: string) => void;
+ onNewGithubTokenChange: (value: string) => void;
+ onNewGithubDescriptionChange: (value: string) => void;
+ onToggleNewTokenVisibility: () => void;
+ onCreateGithubCredential: () => void;
+ onCancelCreateGithubCredential: () => void;
+ onToggleGithubCredential: (credentialId: string, isActive: boolean) => void;
+ onDeleteGithubCredential: (credentialId: string) => void;
+};
+
+export default function GithubCredentialsSection({
+ githubCredentials,
+ showNewGithubForm,
+ showNewTokenPlainText,
+ newGithubName,
+ newGithubToken,
+ newGithubDescription,
+ onShowNewGithubFormChange,
+ onNewGithubNameChange,
+ onNewGithubTokenChange,
+ onNewGithubDescriptionChange,
+ onToggleNewTokenVisibility,
+ onCreateGithubCredential,
+ onCancelCreateGithubCredential,
+ onToggleGithubCredential,
+ onDeleteGithubCredential,
+}: GithubCredentialsSectionProps) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+
+
+
{t('apiKeys.github.title')}
+
+
+
+
+
{t('apiKeys.github.descriptionAlt')}
+
+ {showNewGithubForm && (
+
+ )}
+
+
+ {githubCredentials.length === 0 ? (
+
{t('apiKeys.github.empty')}
+ ) : (
+ githubCredentials.map((credential) => (
+
+
+
{credential.credential_name}
+ {credential.description && (
+
{credential.description}
+ )}
+
+ {t('apiKeys.github.added')} {new Date(credential.created_at).toLocaleDateString()}
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+ );
+}
diff --git a/src/components/settings/view/tabs/api-settings/sections/NewApiKeyAlert.tsx b/src/components/settings/view/tabs/api-settings/sections/NewApiKeyAlert.tsx
new file mode 100644
index 0000000..9d78746
--- /dev/null
+++ b/src/components/settings/view/tabs/api-settings/sections/NewApiKeyAlert.tsx
@@ -0,0 +1,42 @@
+import { Check, Copy } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Button } from '../../../../../ui/button';
+import type { CreatedApiKey } from '../types';
+
+type NewApiKeyAlertProps = {
+ apiKey: CreatedApiKey;
+ copiedKey: string | null;
+ onCopy: (text: string, id: string) => void;
+ onDismiss: () => void;
+};
+
+export default function NewApiKeyAlert({
+ apiKey,
+ copiedKey,
+ onCopy,
+ onDismiss,
+}: NewApiKeyAlertProps) {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
{t('apiKeys.newKey.alertTitle')}
+
{t('apiKeys.newKey.alertMessage')}
+
+
+ {apiKey.apiKey}
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx b/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx
new file mode 100644
index 0000000..47ff62b
--- /dev/null
+++ b/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx
@@ -0,0 +1,46 @@
+import { ExternalLink } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import type { ReleaseInfo } from '../../../../../../types/sharedTypes';
+
+type VersionInfoSectionProps = {
+ currentVersion: string;
+ updateAvailable: boolean;
+ latestVersion: string | null;
+ releaseInfo: ReleaseInfo | null;
+};
+
+export default function VersionInfoSection({
+ currentVersion,
+ updateAvailable,
+ latestVersion,
+ releaseInfo,
+}: VersionInfoSectionProps) {
+ const { t } = useTranslation('settings');
+ const releasesUrl = releaseInfo?.htmlUrl || 'https://github.com/siteboon/claudecodeui/releases';
+
+ return (
+
+ );
+}
diff --git a/src/components/settings/view/tabs/api-settings/types.ts b/src/components/settings/view/tabs/api-settings/types.ts
new file mode 100644
index 0000000..b3d3ae1
--- /dev/null
+++ b/src/components/settings/view/tabs/api-settings/types.ts
@@ -0,0 +1,36 @@
+export type ApiKeyItem = {
+ id: string;
+ key_name: string;
+ api_key: string;
+ created_at: string;
+ last_used?: string | null;
+ is_active: boolean;
+};
+
+export type CreatedApiKey = {
+ id: string;
+ keyName: string;
+ apiKey: string;
+ createdAt?: string;
+};
+
+export type GithubCredentialItem = {
+ id: string;
+ credential_name: string;
+ description?: string | null;
+ created_at: string;
+ is_active: boolean;
+};
+
+export type ApiKeysResponse = {
+ apiKeys?: ApiKeyItem[];
+ success?: boolean;
+ error?: string;
+ apiKey?: CreatedApiKey;
+};
+
+export type GithubCredentialsResponse = {
+ credentials?: GithubCredentialItem[];
+ success?: boolean;
+ error?: string;
+};
diff --git a/src/components/settings/view/tabs/git-settings/GitSettingsTab.tsx b/src/components/settings/view/tabs/git-settings/GitSettingsTab.tsx
new file mode 100644
index 0000000..84f633a
--- /dev/null
+++ b/src/components/settings/view/tabs/git-settings/GitSettingsTab.tsx
@@ -0,0 +1,82 @@
+import { Check, GitBranch } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { useGitSettings } from '../../../hooks/useGitSettings';
+import { Button } from '../../../../ui/button';
+import { Input } from '../../../../ui/input';
+
+export default function GitSettingsTab() {
+ const { t } = useTranslation('settings');
+ const {
+ gitName,
+ setGitName,
+ gitEmail,
+ setGitEmail,
+ isLoading,
+ isSaving,
+ saveStatus,
+ saveGitConfig,
+ } = useGitSettings();
+
+ return (
+
+
+
+
+
{t('git.title')}
+
+
+
{t('git.description')}
+
+
+
+
+
setGitName(event.target.value)}
+ placeholder="John Doe"
+ disabled={isLoading}
+ className="w-full"
+ />
+
{t('git.name.help')}
+
+
+
+
+
setGitEmail(event.target.value)}
+ placeholder="john@example.com"
+ disabled={isLoading}
+ className="w-full"
+ />
+
{t('git.email.help')}
+
+
+
+
+
+ {saveStatus === 'success' && (
+
+
+ {t('git.status.success')}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx b/src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx
new file mode 100644
index 0000000..55423e1
--- /dev/null
+++ b/src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx
@@ -0,0 +1,106 @@
+import { useTranslation } from 'react-i18next';
+import { useTasksSettings } from '../../../../../contexts/TasksSettingsContext';
+
+type TasksSettingsContextValue = {
+ tasksEnabled: boolean;
+ setTasksEnabled: (enabled: boolean) => void;
+ isTaskMasterInstalled: boolean | null;
+ isCheckingInstallation: boolean;
+};
+
+export default function TasksSettingsTab() {
+ const { t } = useTranslation('settings');
+ const {
+ tasksEnabled,
+ setTasksEnabled,
+ isTaskMasterInstalled,
+ isCheckingInstallation,
+ } = useTasksSettings() as TasksSettingsContextValue;
+
+ return (
+
+ {isCheckingInstallation ? (
+
+
+
+
{t('tasks.checking')}
+
+
+ ) : (
+ <>
+ {!isTaskMasterInstalled && (
+
+
+
+
+
+ {t('tasks.notInstalled.title')}
+
+
+
{t('tasks.notInstalled.description')}
+
+
+ {t('tasks.notInstalled.installCommand')}
+
+
+
+
+
+
{t('tasks.notInstalled.afterInstallation')}
+
+ - {t('tasks.notInstalled.steps.restart')}
+ - {t('tasks.notInstalled.steps.autoAvailable')}
+ - {t('tasks.notInstalled.steps.initCommand')}
+
+
+
+
+
+
+ )}
+
+ {isTaskMasterInstalled && (
+
+
+
+
+
{t('tasks.settings.enableLabel')}
+
{t('tasks.settings.enableDescription')}
+
+
+
+
+
+ )}
+ >
+ )}
+
+ );
+}