mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-05 04:15:42 +08:00
Merge branch 'main' into fix/numerous-bug-fixes
This commit is contained in:
@@ -62,10 +62,19 @@ function validateBranchName(branch) {
|
|||||||
return branch;
|
return branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateFilePath(file) {
|
function validateFilePath(file, projectPath) {
|
||||||
if (!file || file.includes('\0')) {
|
if (!file || file.includes('\0')) {
|
||||||
throw new Error('Invalid file path');
|
throw new Error('Invalid file path');
|
||||||
}
|
}
|
||||||
|
// Prevent path traversal: resolve the file relative to the project root
|
||||||
|
// and ensure the result stays within the project directory
|
||||||
|
if (projectPath) {
|
||||||
|
const resolved = path.resolve(projectPath, file);
|
||||||
|
const normalizedRoot = path.resolve(projectPath) + path.sep;
|
||||||
|
if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectPath)) {
|
||||||
|
throw new Error('Invalid file path: path traversal detected');
|
||||||
|
}
|
||||||
|
}
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +85,32 @@ function validateRemoteName(remote) {
|
|||||||
return remote;
|
return remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateProjectPath(projectPath) {
|
||||||
|
if (!projectPath || projectPath.includes('\0')) {
|
||||||
|
throw new Error('Invalid project path');
|
||||||
|
}
|
||||||
|
const resolved = path.resolve(projectPath);
|
||||||
|
// Must be an absolute path after resolution
|
||||||
|
if (!path.isAbsolute(resolved)) {
|
||||||
|
throw new Error('Invalid project path: must be absolute');
|
||||||
|
}
|
||||||
|
// Block obviously dangerous paths
|
||||||
|
if (resolved === '/' || resolved === path.sep) {
|
||||||
|
throw new Error('Invalid project path: root directory not allowed');
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get the actual project path from the encoded project name
|
// Helper function to get the actual project path from the encoded project name
|
||||||
async function getActualProjectPath(projectName) {
|
async function getActualProjectPath(projectName) {
|
||||||
|
let projectPath;
|
||||||
try {
|
try {
|
||||||
return await extractProjectDirectory(projectName);
|
projectPath = await extractProjectDirectory(projectName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error extracting project directory for ${projectName}:`, error);
|
console.error(`Error extracting project directory for ${projectName}:`, error);
|
||||||
throw new Error(`Unable to resolve project path for "${projectName}"`);
|
throw new Error(`Unable to resolve project path for "${projectName}"`);
|
||||||
}
|
}
|
||||||
|
return validateProjectPath(projectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to strip git diff headers
|
// Helper function to strip git diff headers
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||||
import type { MainContentHeaderProps } from '../../types/types';
|
import type { MainContentHeaderProps } from '../../types/types';
|
||||||
import MobileMenuButton from './MobileMenuButton';
|
import MobileMenuButton from './MobileMenuButton';
|
||||||
import MainContentTabSwitcher from './MainContentTabSwitcher';
|
import MainContentTabSwitcher from './MainContentTabSwitcher';
|
||||||
@@ -12,6 +13,26 @@ export default function MainContentHeader({
|
|||||||
isMobile,
|
isMobile,
|
||||||
onMenuClick,
|
onMenuClick,
|
||||||
}: MainContentHeaderProps) {
|
}: MainContentHeaderProps) {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||||
|
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||||
|
|
||||||
|
const updateScrollState = useCallback(() => {
|
||||||
|
const el = scrollRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
setCanScrollLeft(el.scrollLeft > 2);
|
||||||
|
setCanScrollRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 2);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = scrollRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
updateScrollState();
|
||||||
|
const observer = new ResizeObserver(updateScrollState);
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [updateScrollState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pwa-header-safe flex-shrink-0 border-b border-border/60 bg-background px-3 py-1.5 sm:px-4 sm:py-2">
|
<div className="pwa-header-safe flex-shrink-0 border-b border-border/60 bg-background px-3 py-1.5 sm:px-4 sm:py-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -25,12 +46,24 @@ export default function MainContentHeader({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden flex-shrink-0 sm:block">
|
<div className="relative min-w-0 flex-shrink overflow-hidden sm:flex-shrink-0">
|
||||||
<MainContentTabSwitcher
|
{canScrollLeft && (
|
||||||
activeTab={activeTab}
|
<div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-gradient-to-r from-background to-transparent" />
|
||||||
setActiveTab={setActiveTab}
|
)}
|
||||||
shouldShowTasksTab={shouldShowTasksTab}
|
<div
|
||||||
/>
|
ref={scrollRef}
|
||||||
|
onScroll={updateScrollState}
|
||||||
|
className="scrollbar-hide overflow-x-auto"
|
||||||
|
>
|
||||||
|
<MainContentTabSwitcher
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
shouldShowTasksTab={shouldShowTasksTab}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{canScrollRight && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-gradient-to-l from-background to-transparent" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
|
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Tooltip } from '../../../../shared/view/ui';
|
import { Tooltip, PillBar, Pill } from '../../../../shared/view/ui';
|
||||||
import type { AppTab } from '../../../../types/app';
|
import type { AppTab } from '../../../../types/app';
|
||||||
import { usePlugins } from '../../../../contexts/PluginsContext';
|
import { usePlugins } from '../../../../contexts/PluginsContext';
|
||||||
import PluginIcon from '../../../plugins/view/PluginIcon';
|
import PluginIcon from '../../../plugins/view/PluginIcon';
|
||||||
@@ -66,20 +66,17 @@ export default function MainContentTabSwitcher({
|
|||||||
const tabs: TabDefinition[] = [...builtInTabs, ...pluginTabs];
|
const tabs: TabDefinition[] = [...builtInTabs, ...pluginTabs];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-[2px] rounded-lg bg-muted/60 p-[3px]">
|
<PillBar>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = tab.id === activeTab;
|
const isActive = tab.id === activeTab;
|
||||||
const displayLabel = tab.kind === 'builtin' ? t(tab.labelKey) : tab.label;
|
const displayLabel = tab.kind === 'builtin' ? t(tab.labelKey) : tab.label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={tab.id} content={displayLabel} position="bottom">
|
<Tooltip key={tab.id} content={displayLabel} position="bottom">
|
||||||
<button
|
<Pill
|
||||||
|
isActive={isActive}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={`relative flex items-center gap-1.5 rounded-md px-2.5 py-[5px] text-sm font-medium transition-all duration-150 ${
|
className="px-2.5 py-[5px]"
|
||||||
isActive
|
|
||||||
? 'bg-background text-foreground shadow-sm'
|
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{tab.kind === 'builtin' ? (
|
{tab.kind === 'builtin' ? (
|
||||||
<tab.icon className="h-3.5 w-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
|
<tab.icon className="h-3.5 w-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
|
||||||
@@ -91,10 +88,10 @@ export default function MainContentTabSwitcher({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="hidden lg:inline">{displayLabel}</span>
|
<span className="hidden lg:inline">{displayLabel}</span>
|
||||||
</button>
|
</Pill>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</PillBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
const closeTimerRef = useRef<number | null>(null);
|
const closeTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<SettingsMainTab>(() => normalizeMainTab(initialTab));
|
const [activeTab, setActiveTab] = useState<SettingsMainTab>(() => normalizeMainTab(initialTab));
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null);
|
const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null);
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
const [projectSortOrder, setProjectSortOrder] = useState<ProjectSortOrder>('name');
|
const [projectSortOrder, setProjectSortOrder] = useState<ProjectSortOrder>('name');
|
||||||
@@ -701,9 +700,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
}, [checkAuthStatus, loginProvider]);
|
}, [checkAuthStatus, loginProvider]);
|
||||||
|
|
||||||
const saveSettings = useCallback(() => {
|
const saveSettings = useCallback(() => {
|
||||||
setIsSaving(true);
|
|
||||||
setSaveStatus(null);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
localStorage.setItem('claude-settings', JSON.stringify({
|
localStorage.setItem('claude-settings', JSON.stringify({
|
||||||
@@ -732,16 +728,9 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setSaveStatus('success');
|
setSaveStatus('success');
|
||||||
if (closeTimerRef.current !== null) {
|
|
||||||
window.clearTimeout(closeTimerRef.current);
|
|
||||||
closeTimerRef.current = null;
|
|
||||||
}
|
|
||||||
closeTimerRef.current = window.setTimeout(() => onClose(), 1000);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving settings:', error);
|
console.error('Error saving settings:', error);
|
||||||
setSaveStatus('error');
|
setSaveStatus('error');
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
claudePermissions.allowedTools,
|
claudePermissions.allowedTools,
|
||||||
@@ -751,7 +740,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
cursorPermissions.allowedCommands,
|
cursorPermissions.allowedCommands,
|
||||||
cursorPermissions.disallowedCommands,
|
cursorPermissions.disallowedCommands,
|
||||||
cursorPermissions.skipPermissions,
|
cursorPermissions.skipPermissions,
|
||||||
onClose,
|
geminiPermissionMode,
|
||||||
projectSortOrder,
|
projectSortOrder,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -804,11 +793,58 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||||
}, [codeEditorSettings]);
|
}, [codeEditorSettings]);
|
||||||
|
|
||||||
|
// Auto-save permissions and sort order with debounce
|
||||||
|
const autoSaveTimerRef = useRef<number | null>(null);
|
||||||
|
const isInitialLoadRef = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip auto-save on initial load (settings are being loaded from localStorage)
|
||||||
|
if (isInitialLoadRef.current) {
|
||||||
|
isInitialLoadRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoSaveTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(autoSaveTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
autoSaveTimerRef.current = window.setTimeout(() => {
|
||||||
|
saveSettings();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoSaveTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(autoSaveTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [saveSettings]);
|
||||||
|
|
||||||
|
// Clear save status after 2 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (saveStatus === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => setSaveStatus(null), 2000);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [saveStatus]);
|
||||||
|
|
||||||
|
// Reset initial load flag when settings dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
isInitialLoadRef.current = true;
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (closeTimerRef.current !== null) {
|
if (closeTimerRef.current !== null) {
|
||||||
window.clearTimeout(closeTimerRef.current);
|
window.clearTimeout(closeTimerRef.current);
|
||||||
closeTimerRef.current = null;
|
closeTimerRef.current = null;
|
||||||
}
|
}
|
||||||
|
if (autoSaveTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(autoSaveTimerRef.current);
|
||||||
|
autoSaveTimerRef.current = null;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -816,7 +852,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
setActiveTab,
|
setActiveTab,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
toggleDarkMode,
|
toggleDarkMode,
|
||||||
isSaving,
|
|
||||||
saveStatus,
|
saveStatus,
|
||||||
deleteError,
|
deleteError,
|
||||||
projectSortOrder,
|
projectSortOrder,
|
||||||
@@ -861,6 +896,5 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
loginProvider,
|
loginProvider,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
handleLoginComplete,
|
handleLoginComplete,
|
||||||
saveSettings,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Settings as SettingsIcon, X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
|
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
|
||||||
import { Button } from '../../../shared/view/ui';
|
import { Button } from '../../../shared/view/ui';
|
||||||
import ClaudeMcpFormModal from '../view/modals/ClaudeMcpFormModal';
|
import ClaudeMcpFormModal from '../view/modals/ClaudeMcpFormModal';
|
||||||
import CodexMcpFormModal from '../view/modals/CodexMcpFormModal';
|
import CodexMcpFormModal from '../view/modals/CodexMcpFormModal';
|
||||||
import SettingsMainTabs from '../view/SettingsMainTabs';
|
import SettingsSidebar from '../view/SettingsSidebar';
|
||||||
import AgentsSettingsTab from '../view/tabs/agents-settings/AgentsSettingsTab';
|
import AgentsSettingsTab from '../view/tabs/agents-settings/AgentsSettingsTab';
|
||||||
import AppearanceSettingsTab from '../view/tabs/AppearanceSettingsTab';
|
import AppearanceSettingsTab from '../view/tabs/AppearanceSettingsTab';
|
||||||
import CredentialsSettingsTab from '../view/tabs/api-settings/CredentialsSettingsTab';
|
import CredentialsSettingsTab from '../view/tabs/api-settings/CredentialsSettingsTab';
|
||||||
@@ -19,7 +19,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
const {
|
const {
|
||||||
activeTab,
|
activeTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
isSaving,
|
|
||||||
saveStatus,
|
saveStatus,
|
||||||
deleteError,
|
deleteError,
|
||||||
projectSortOrder,
|
projectSortOrder,
|
||||||
@@ -64,7 +63,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
loginProvider,
|
loginProvider,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
handleLoginComplete,
|
handleLoginComplete,
|
||||||
saveSettings,
|
|
||||||
} = useSettingsController({
|
} = useSettingsController({
|
||||||
isOpen,
|
isOpen,
|
||||||
initialTab,
|
initialTab,
|
||||||
@@ -85,140 +83,90 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
: false;
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-backdrop fixed inset-0 z-[9999] flex items-center justify-center bg-background/95 md:p-4">
|
<div className="modal-backdrop fixed inset-0 z-[9999] flex items-center justify-center bg-background/80 backdrop-blur-sm md:p-4">
|
||||||
<div className="flex h-full w-full flex-col border border-border bg-background shadow-xl md:h-[90vh] md:max-w-4xl md:rounded-lg">
|
<div className="flex h-full w-full flex-col overflow-hidden border border-border bg-background shadow-2xl md:h-[90vh] md:max-w-4xl md:rounded-xl">
|
||||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border p-4 md:p-6">
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-4 py-3 md:px-5">
|
||||||
<SettingsIcon className="h-5 w-5 text-blue-600 md:h-6 md:w-6" />
|
<h2 className="text-base font-semibold text-foreground">{t('title')}</h2>
|
||||||
<h2 className="text-lg font-semibold text-foreground md:text-xl">{t('title')}</h2>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={onClose}
|
|
||||||
className="touch-manipulation text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<SettingsMainTabs activeTab={activeTab} onChange={setActiveTab} />
|
|
||||||
|
|
||||||
<div className="space-y-6 p-4 pb-safe-area-inset-bottom md:space-y-8 md:p-6">
|
|
||||||
{activeTab === 'appearance' && (
|
|
||||||
<AppearanceSettingsTab
|
|
||||||
projectSortOrder={projectSortOrder}
|
|
||||||
onProjectSortOrderChange={setProjectSortOrder}
|
|
||||||
codeEditorSettings={codeEditorSettings}
|
|
||||||
onCodeEditorThemeChange={(value) => 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' && <GitSettingsTab />}
|
|
||||||
|
|
||||||
{activeTab === 'agents' && (
|
|
||||||
<AgentsSettingsTab
|
|
||||||
claudeAuthStatus={claudeAuthStatus}
|
|
||||||
cursorAuthStatus={cursorAuthStatus}
|
|
||||||
codexAuthStatus={codexAuthStatus}
|
|
||||||
geminiAuthStatus={geminiAuthStatus}
|
|
||||||
onClaudeLogin={() => openLoginForProvider('claude')}
|
|
||||||
onCursorLogin={() => openLoginForProvider('cursor')}
|
|
||||||
onCodexLogin={() => openLoginForProvider('codex')}
|
|
||||||
onGeminiLogin={() => openLoginForProvider('gemini')}
|
|
||||||
claudePermissions={claudePermissions}
|
|
||||||
onClaudePermissionsChange={setClaudePermissions}
|
|
||||||
cursorPermissions={cursorPermissions}
|
|
||||||
onCursorPermissionsChange={setCursorPermissions}
|
|
||||||
codexPermissionMode={codexPermissionMode}
|
|
||||||
onCodexPermissionModeChange={setCodexPermissionMode}
|
|
||||||
geminiPermissionMode={geminiPermissionMode}
|
|
||||||
onGeminiPermissionModeChange={setGeminiPermissionMode}
|
|
||||||
mcpServers={mcpServers}
|
|
||||||
cursorMcpServers={cursorMcpServers}
|
|
||||||
codexMcpServers={codexMcpServers}
|
|
||||||
mcpTestResults={mcpTestResults}
|
|
||||||
mcpServerTools={mcpServerTools}
|
|
||||||
mcpToolsLoading={mcpToolsLoading}
|
|
||||||
onOpenMcpForm={openMcpForm}
|
|
||||||
onDeleteMcpServer={handleMcpDelete}
|
|
||||||
onTestMcpServer={handleMcpTest}
|
|
||||||
onDiscoverMcpTools={handleMcpToolsDiscovery}
|
|
||||||
onOpenCodexMcpForm={openCodexMcpForm}
|
|
||||||
onDeleteCodexMcpServer={handleCodexMcpDelete}
|
|
||||||
deleteError={deleteError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'tasks' && (
|
|
||||||
<div className="space-y-6 md:space-y-8">
|
|
||||||
<TasksSettingsTab />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'api' && (
|
|
||||||
<div className="space-y-6 md:space-y-8">
|
|
||||||
<CredentialsSettingsTab />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'plugins' && (
|
|
||||||
<div className="space-y-6 md:space-y-8">
|
|
||||||
<PluginSettingsTab />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-shrink-0 flex-col gap-3 border-t border-border p-4 pb-safe-area-inset-bottom sm:flex-row sm:items-center sm:justify-between md:p-6">
|
|
||||||
<div className="order-2 flex items-center justify-center gap-2 sm:order-1 sm:justify-start">
|
|
||||||
{saveStatus === 'success' && (
|
{saveStatus === 'success' && (
|
||||||
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
|
<span className="text-xs text-muted-foreground animate-in fade-in">{t('saveStatus.success')}</span>
|
||||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{t('saveStatus.success')}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{saveStatus === 'error' && (
|
|
||||||
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
|
|
||||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{t('saveStatus.error')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="order-1 flex items-center gap-3 sm:order-2">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={isSaving}
|
className="h-10 w-10 touch-manipulation p-0 text-muted-foreground hover:text-foreground active:bg-accent/50"
|
||||||
className="h-10 flex-1 touch-manipulation sm:flex-none"
|
|
||||||
>
|
>
|
||||||
{t('footerActions.cancel')}
|
<X className="h-5 w-5" />
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={saveSettings}
|
|
||||||
disabled={isSaving}
|
|
||||||
className="h-10 flex-1 touch-manipulation bg-blue-600 hover:bg-blue-700 disabled:opacity-50 sm:flex-none"
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
||||||
{t('saveStatus.saving')}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
t('footerActions.save')
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Body: sidebar + content */}
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
||||||
|
<SettingsSidebar activeTab={activeTab} onChange={setActiveTab} />
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<main className="flex-1 overflow-y-auto">
|
||||||
|
<div key={activeTab} className="settings-content-enter space-y-6 p-4 pb-safe-area-inset-bottom md:space-y-8 md:p-6">
|
||||||
|
{activeTab === 'appearance' && (
|
||||||
|
<AppearanceSettingsTab
|
||||||
|
projectSortOrder={projectSortOrder}
|
||||||
|
onProjectSortOrderChange={setProjectSortOrder}
|
||||||
|
codeEditorSettings={codeEditorSettings}
|
||||||
|
onCodeEditorThemeChange={(value) => 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' && <GitSettingsTab />}
|
||||||
|
|
||||||
|
{activeTab === 'agents' && (
|
||||||
|
<AgentsSettingsTab
|
||||||
|
claudeAuthStatus={claudeAuthStatus}
|
||||||
|
cursorAuthStatus={cursorAuthStatus}
|
||||||
|
codexAuthStatus={codexAuthStatus}
|
||||||
|
geminiAuthStatus={geminiAuthStatus}
|
||||||
|
onClaudeLogin={() => openLoginForProvider('claude')}
|
||||||
|
onCursorLogin={() => openLoginForProvider('cursor')}
|
||||||
|
onCodexLogin={() => openLoginForProvider('codex')}
|
||||||
|
onGeminiLogin={() => openLoginForProvider('gemini')}
|
||||||
|
claudePermissions={claudePermissions}
|
||||||
|
onClaudePermissionsChange={setClaudePermissions}
|
||||||
|
cursorPermissions={cursorPermissions}
|
||||||
|
onCursorPermissionsChange={setCursorPermissions}
|
||||||
|
codexPermissionMode={codexPermissionMode}
|
||||||
|
onCodexPermissionModeChange={setCodexPermissionMode}
|
||||||
|
geminiPermissionMode={geminiPermissionMode}
|
||||||
|
onGeminiPermissionModeChange={setGeminiPermissionMode}
|
||||||
|
mcpServers={mcpServers}
|
||||||
|
cursorMcpServers={cursorMcpServers}
|
||||||
|
codexMcpServers={codexMcpServers}
|
||||||
|
mcpTestResults={mcpTestResults}
|
||||||
|
mcpServerTools={mcpServerTools}
|
||||||
|
mcpToolsLoading={mcpToolsLoading}
|
||||||
|
onOpenMcpForm={openMcpForm}
|
||||||
|
onDeleteMcpServer={handleMcpDelete}
|
||||||
|
onTestMcpServer={handleMcpTest}
|
||||||
|
onDiscoverMcpTools={handleMcpToolsDiscovery}
|
||||||
|
onOpenCodexMcpForm={openCodexMcpForm}
|
||||||
|
onDeleteCodexMcpServer={handleCodexMcpDelete}
|
||||||
|
deleteError={deleteError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'tasks' && <TasksSettingsTab />}
|
||||||
|
|
||||||
|
{activeTab === 'api' && <CredentialsSettingsTab />}
|
||||||
|
|
||||||
|
{activeTab === 'plugins' && <PluginSettingsTab />}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProviderLoginModal
|
<ProviderLoginModal
|
||||||
|
|||||||
22
src/components/settings/view/SettingsCard.tsx
Normal file
22
src/components/settings/view/SettingsCard.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { cn } from '../../../lib/utils';
|
||||||
|
|
||||||
|
type SettingsCardProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
divided?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsCard({ children, className, divided }: SettingsCardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-xl border border-border bg-card/50',
|
||||||
|
divided && 'divide-y divide-border',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/components/settings/view/SettingsRow.tsx
Normal file
23
src/components/settings/view/SettingsRow.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { cn } from '../../../lib/utils';
|
||||||
|
|
||||||
|
type SettingsRowProps = {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsRow({ label, description, children, className }: SettingsRowProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center justify-between gap-4 px-4 py-4', className)}>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-sm font-medium text-foreground">{label}</div>
|
||||||
|
{description && (
|
||||||
|
<div className="mt-0.5 text-sm text-muted-foreground">{description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/components/settings/view/SettingsSection.tsx
Normal file
25
src/components/settings/view/SettingsSection.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { cn } from '../../../lib/utils';
|
||||||
|
|
||||||
|
type SettingsSectionProps = {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsSection({ title, description, children, className }: SettingsSectionProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-3', className)}>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/components/settings/view/SettingsSidebar.tsx
Normal file
80
src/components/settings/view/SettingsSidebar.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Bot, GitBranch, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { cn } from '../../../lib/utils';
|
||||||
|
import { PillBar, Pill } from '../../../shared/view/ui';
|
||||||
|
import type { SettingsMainTab } from '../types/types';
|
||||||
|
|
||||||
|
type SettingsSidebarProps = {
|
||||||
|
activeTab: SettingsMainTab;
|
||||||
|
onChange: (tab: SettingsMainTab) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavItem = {
|
||||||
|
id: SettingsMainTab;
|
||||||
|
labelKey: string;
|
||||||
|
icon: typeof Bot;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NAV_ITEMS: NavItem[] = [
|
||||||
|
{ id: 'agents', labelKey: 'mainTabs.agents', icon: Bot },
|
||||||
|
{ id: 'appearance', labelKey: 'mainTabs.appearance', icon: Palette },
|
||||||
|
{ id: 'git', labelKey: 'mainTabs.git', icon: GitBranch },
|
||||||
|
{ id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key },
|
||||||
|
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
|
||||||
|
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Desktop sidebar */}
|
||||||
|
<aside className="hidden w-56 flex-shrink-0 border-r border-border bg-muted/30 md:flex md:flex-col">
|
||||||
|
<nav className="flex flex-col gap-1 p-3">
|
||||||
|
{NAV_ITEMS.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = activeTab === item.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => onChange(item.id)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium transition-colors duration-150',
|
||||||
|
isActive
|
||||||
|
? 'bg-accent text-accent-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground active:bg-accent/50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||||
|
{t(item.labelKey)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Mobile horizontal nav — pill bar */}
|
||||||
|
<div className="flex-shrink-0 border-b border-border px-3 py-2 md:hidden">
|
||||||
|
<PillBar className="scrollbar-hide w-full overflow-x-auto">
|
||||||
|
{NAV_ITEMS.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pill
|
||||||
|
key={item.id}
|
||||||
|
isActive={activeTab === item.id}
|
||||||
|
onClick={() => onChange(item.id)}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<Icon className="h-3.5 w-3.5" />
|
||||||
|
{t(item.labelKey)}
|
||||||
|
</Pill>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PillBar>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
src/components/settings/view/SettingsToggle.tsx
Normal file
34
src/components/settings/view/SettingsToggle.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { cn } from '../../../lib/utils';
|
||||||
|
|
||||||
|
type SettingsToggleProps = {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
ariaLabel: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsToggle({ checked, onChange, ariaLabel, disabled }: SettingsToggleProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => onChange(!checked)}
|
||||||
|
className={cn(
|
||||||
|
'relative inline-flex h-7 w-12 flex-shrink-0 touch-manipulation cursor-pointer items-center rounded-full border-2 transition-colors duration-200',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
||||||
|
checked ? 'border-primary bg-primary' : 'border-border bg-muted',
|
||||||
|
disabled && 'cursor-not-allowed opacity-50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none inline-block h-5 w-5 rounded-full shadow-sm transition-transform duration-200',
|
||||||
|
checked ? 'translate-x-[22px] bg-white' : 'translate-x-[2px] bg-foreground/60 dark:bg-foreground/80',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DarkModeToggle } from '../../../../shared/view/ui';
|
import { DarkModeToggle } from '../../../../shared/view/ui';
|
||||||
import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types';
|
import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types';
|
||||||
import LanguageSelector from '../../../../shared/view/ui/LanguageSelector';
|
import LanguageSelector from '../../../../shared/view/ui/LanguageSelector';
|
||||||
|
import SettingsCard from '../SettingsCard';
|
||||||
|
import SettingsRow from '../SettingsRow';
|
||||||
|
import SettingsSection from '../SettingsSection';
|
||||||
|
import SettingsToggle from '../SettingsToggle';
|
||||||
|
|
||||||
type AppearanceSettingsTabProps = {
|
type AppearanceSettingsTabProps = {
|
||||||
projectSortOrder: ProjectSortOrder;
|
projectSortOrder: ProjectSortOrder;
|
||||||
@@ -15,52 +18,6 @@ type AppearanceSettingsTabProps = {
|
|||||||
onCodeEditorFontSizeChange: (value: string) => 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="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
|
||||||
<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 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 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'
|
|
||||||
} flex h-6 w-6 transform items-center justify-center rounded-full bg-white shadow-lg transition-transform duration-200`}
|
|
||||||
>
|
|
||||||
{checked ? onIcon : offIcon}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppearanceSettingsTab({
|
export default function AppearanceSettingsTab({
|
||||||
projectSortOrder,
|
projectSortOrder,
|
||||||
onProjectSortOrderChange,
|
onProjectSortOrderChange,
|
||||||
@@ -72,108 +29,98 @@ export default function AppearanceSettingsTab({
|
|||||||
onCodeEditorFontSizeChange,
|
onCodeEditorFontSizeChange,
|
||||||
}: AppearanceSettingsTabProps) {
|
}: AppearanceSettingsTabProps) {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const codeEditorThemeLabel = t('appearanceSettings.codeEditor.theme.label');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 md:space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4">
|
<SettingsSection title={t('appearanceSettings.darkMode.label')}>
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<SettingsCard>
|
||||||
<div className="flex items-center justify-between">
|
<SettingsRow
|
||||||
<div>
|
label={t('appearanceSettings.darkMode.label')}
|
||||||
<div className="font-medium text-foreground">{t('appearanceSettings.darkMode.label')}</div>
|
description={t('appearanceSettings.darkMode.description')}
|
||||||
<div className="text-sm text-muted-foreground">
|
>
|
||||||
{t('appearanceSettings.darkMode.description')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DarkModeToggle ariaLabel={t('appearanceSettings.darkMode.label')} />
|
<DarkModeToggle ariaLabel={t('appearanceSettings.darkMode.label')} />
|
||||||
</div>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsCard>
|
||||||
</div>
|
</SettingsSection>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<SettingsSection title={t('mainTabs.appearance')}>
|
||||||
<LanguageSelector />
|
<SettingsCard>
|
||||||
</div>
|
<LanguageSelector />
|
||||||
|
</SettingsCard>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<SettingsSection title={t('appearanceSettings.projectSorting.label')}>
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<SettingsCard>
|
||||||
<div className="flex items-center justify-between">
|
<SettingsRow
|
||||||
<div>
|
label={t('appearanceSettings.projectSorting.label')}
|
||||||
<div className="font-medium text-foreground">
|
description={t('appearanceSettings.projectSorting.description')}
|
||||||
{t('appearanceSettings.projectSorting.label')}
|
>
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t('appearanceSettings.projectSorting.description')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<select
|
<select
|
||||||
value={projectSortOrder}
|
value={projectSortOrder}
|
||||||
onChange={(event) => onProjectSortOrderChange(event.target.value as ProjectSortOrder)}
|
onChange={(event) => onProjectSortOrderChange(event.target.value as ProjectSortOrder)}
|
||||||
className="w-32 rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
className="w-full rounded-lg border border-input bg-card p-2.5 text-sm text-foreground touch-manipulation focus:border-primary focus:ring-1 focus:ring-primary sm:w-36"
|
||||||
>
|
>
|
||||||
<option value="name">{t('appearanceSettings.projectSorting.alphabetical')}</option>
|
<option value="name">{t('appearanceSettings.projectSorting.alphabetical')}</option>
|
||||||
<option value="date">{t('appearanceSettings.projectSorting.recentActivity')}</option>
|
<option value="date">{t('appearanceSettings.projectSorting.recentActivity')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsCard>
|
||||||
</div>
|
</SettingsSection>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<SettingsSection title={t('appearanceSettings.codeEditor.title')}>
|
||||||
<h3 className="text-lg font-semibold text-foreground">{t('appearanceSettings.codeEditor.title')}</h3>
|
<SettingsCard divided>
|
||||||
|
<SettingsRow
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
label={t('appearanceSettings.codeEditor.theme.label')}
|
||||||
<div className="flex items-center justify-between">
|
description={t('appearanceSettings.codeEditor.theme.description')}
|
||||||
<div>
|
>
|
||||||
<div className="font-medium text-foreground">{codeEditorThemeLabel}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t('appearanceSettings.codeEditor.theme.description')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DarkModeToggle
|
<DarkModeToggle
|
||||||
checked={codeEditorSettings.theme === 'dark'}
|
checked={codeEditorSettings.theme === 'dark'}
|
||||||
onToggle={(enabled) => onCodeEditorThemeChange(enabled ? 'dark' : 'light')}
|
onToggle={(enabled) => onCodeEditorThemeChange(enabled ? 'dark' : 'light')}
|
||||||
ariaLabel={codeEditorThemeLabel}
|
ariaLabel={t('appearanceSettings.codeEditor.theme.label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</SettingsRow>
|
||||||
</div>
|
|
||||||
|
|
||||||
<ToggleCard
|
<SettingsRow
|
||||||
label={t('appearanceSettings.codeEditor.wordWrap.label')}
|
label={t('appearanceSettings.codeEditor.wordWrap.label')}
|
||||||
description={t('appearanceSettings.codeEditor.wordWrap.description')}
|
description={t('appearanceSettings.codeEditor.wordWrap.description')}
|
||||||
checked={codeEditorSettings.wordWrap}
|
>
|
||||||
onChange={onCodeEditorWordWrapChange}
|
<SettingsToggle
|
||||||
ariaLabel={t('appearanceSettings.codeEditor.wordWrap.label')}
|
checked={codeEditorSettings.wordWrap}
|
||||||
/>
|
onChange={onCodeEditorWordWrapChange}
|
||||||
|
ariaLabel={t('appearanceSettings.codeEditor.wordWrap.label')}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<ToggleCard
|
<SettingsRow
|
||||||
label={t('appearanceSettings.codeEditor.showMinimap.label')}
|
label={t('appearanceSettings.codeEditor.showMinimap.label')}
|
||||||
description={t('appearanceSettings.codeEditor.showMinimap.description')}
|
description={t('appearanceSettings.codeEditor.showMinimap.description')}
|
||||||
checked={codeEditorSettings.showMinimap}
|
>
|
||||||
onChange={onCodeEditorShowMinimapChange}
|
<SettingsToggle
|
||||||
ariaLabel={t('appearanceSettings.codeEditor.showMinimap.label')}
|
checked={codeEditorSettings.showMinimap}
|
||||||
/>
|
onChange={onCodeEditorShowMinimapChange}
|
||||||
|
ariaLabel={t('appearanceSettings.codeEditor.showMinimap.label')}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<ToggleCard
|
<SettingsRow
|
||||||
label={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
label={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
||||||
description={t('appearanceSettings.codeEditor.lineNumbers.description')}
|
description={t('appearanceSettings.codeEditor.lineNumbers.description')}
|
||||||
checked={codeEditorSettings.lineNumbers}
|
>
|
||||||
onChange={onCodeEditorLineNumbersChange}
|
<SettingsToggle
|
||||||
ariaLabel={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
checked={codeEditorSettings.lineNumbers}
|
||||||
/>
|
onChange={onCodeEditorLineNumbersChange}
|
||||||
|
ariaLabel={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<SettingsRow
|
||||||
<div className="flex items-center justify-between">
|
label={t('appearanceSettings.codeEditor.fontSize.label')}
|
||||||
<div>
|
description={t('appearanceSettings.codeEditor.fontSize.description')}
|
||||||
<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
|
<select
|
||||||
value={codeEditorSettings.fontSize}
|
value={codeEditorSettings.fontSize}
|
||||||
onChange={(event) => onCodeEditorFontSizeChange(event.target.value)}
|
onChange={(event) => onCodeEditorFontSizeChange(event.target.value)}
|
||||||
className="w-24 rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
className="w-full rounded-lg border border-input bg-card p-2.5 text-sm text-foreground touch-manipulation focus:border-primary focus:ring-1 focus:ring-primary sm:w-28"
|
||||||
>
|
>
|
||||||
<option value="10">10px</option>
|
<option value="10">10px</option>
|
||||||
<option value="11">11px</option>
|
<option value="11">11px</option>
|
||||||
@@ -185,9 +132,9 @@ export default function AppearanceSettingsTab({
|
|||||||
<option value="18">18px</option>
|
<option value="18">18px</option>
|
||||||
<option value="20">20px</option>
|
<option value="20">20px</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsCard>
|
||||||
</div>
|
</SettingsSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { cn } from '../../../../../lib/utils';
|
||||||
import SessionProviderLogo from '../../../../llm-logo-provider/SessionProviderLogo';
|
import SessionProviderLogo from '../../../../llm-logo-provider/SessionProviderLogo';
|
||||||
import type { AgentProvider, AuthStatus } from '../../../types/types';
|
import type { AgentProvider, AuthStatus } from '../../../types/types';
|
||||||
|
|
||||||
@@ -36,27 +36,15 @@ const agentConfig: Record<AgentProvider, AgentConfig> = {
|
|||||||
|
|
||||||
const colorClasses = {
|
const colorClasses = {
|
||||||
blue: {
|
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',
|
dot: 'bg-blue-500',
|
||||||
},
|
},
|
||||||
purple: {
|
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',
|
dot: 'bg-purple-500',
|
||||||
},
|
},
|
||||||
gray: {
|
gray: {
|
||||||
border: 'border-l-gray-700 dark:border-l-gray-300',
|
dot: 'bg-foreground/60',
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
indigo: {
|
indigo: {
|
||||||
border: 'border-l-indigo-500 md:border-l-indigo-500',
|
|
||||||
borderBottom: 'border-b-indigo-500',
|
|
||||||
bg: 'bg-indigo-50 dark:bg-indigo-900/20',
|
|
||||||
dot: 'bg-indigo-500',
|
dot: 'bg-indigo-500',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@@ -68,7 +56,6 @@ export default function AgentListItem({
|
|||||||
onClick,
|
onClick,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
}: AgentListItemProps) {
|
}: AgentListItemProps) {
|
||||||
const { t } = useTranslation('settings');
|
|
||||||
const config = agentConfig[agentId];
|
const config = agentConfig[agentId];
|
||||||
const colors = colorClasses[config.color];
|
const colors = colorClasses[config.color];
|
||||||
|
|
||||||
@@ -76,16 +63,18 @@ export default function AgentListItem({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`flex-1 border-b-2 px-2 py-3 text-center transition-colors ${isSelected
|
className={cn(
|
||||||
? `${colors.borderBottom} ${colors.bg}`
|
'min-w-0 flex-1 touch-manipulation rounded-md px-2 py-2 text-center transition-all duration-150',
|
||||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
isSelected
|
||||||
}`}
|
? 'bg-background text-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground active:bg-background/50',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex items-center justify-center gap-1.5">
|
||||||
<SessionProviderLogo provider={agentId} className="h-5 w-5" />
|
<SessionProviderLogo provider={agentId} className="h-4 w-4 flex-shrink-0" />
|
||||||
<span className="text-xs font-medium text-foreground">{config.name}</span>
|
<span className="truncate text-xs font-medium">{config.name}</span>
|
||||||
{authStatus.authenticated && (
|
{authStatus.authenticated && (
|
||||||
<span className={`h-1.5 w-1.5 rounded-full ${colors.dot}`} />
|
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${colors.dot}`} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -95,32 +84,20 @@ export default function AgentListItem({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`w-full border-l-4 p-3 text-left transition-colors ${isSelected
|
className={cn(
|
||||||
? `${colors.border} ${colors.bg}`
|
'flex touch-manipulation items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-all duration-150',
|
||||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
isSelected
|
||||||
}`}
|
? 'bg-background text-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground active:bg-background/50',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="mb-1 flex items-center gap-2">
|
<SessionProviderLogo provider={agentId} className="h-4 w-4 flex-shrink-0" />
|
||||||
<SessionProviderLogo provider={agentId} className="h-4 w-4" />
|
<span>{config.name}</span>
|
||||||
<span className="font-medium text-foreground">{config.name}</span>
|
{authStatus.authenticated ? (
|
||||||
</div>
|
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${colors.dot}`} />
|
||||||
<div className="pl-6 text-xs text-muted-foreground">
|
) : authStatus.loading ? (
|
||||||
{authStatus.loading ? (
|
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-muted-foreground/30 animate-pulse" />
|
||||||
<span className="text-gray-400">{t('agents.authStatus.checking')}</span>
|
) : null}
|
||||||
) : authStatus.authenticated ? (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className={`h-1.5 w-1.5 rounded-full ${colors.dot}`} />
|
|
||||||
<span className="max-w-[120px] truncate" title={authStatus.email ?? undefined}>
|
|
||||||
{authStatus.email || t('agents.authStatus.connected')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-gray-400" />
|
|
||||||
<span>{t('agents.authStatus.notConnected')}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function AgentsSettingsTab({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-[400px] flex-col md:min-h-[500px] md:flex-row">
|
<div className="-mx-4 -mb-4 -mt-2 flex min-h-[300px] flex-col overflow-hidden md:-mx-6 md:-mb-6 md:-mt-2 md:min-h-[500px]">
|
||||||
<AgentSelectorSection
|
<AgentSelectorSection
|
||||||
selectedAgent={selectedAgent}
|
selectedAgent={selectedAgent}
|
||||||
onSelectAgent={setSelectedAgent}
|
onSelectAgent={setSelectedAgent}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { cn } from '../../../../../../lib/utils';
|
||||||
import type { AgentCategory } from '../../../../types/types';
|
import type { AgentCategory } from '../../../../types/types';
|
||||||
import type { AgentCategoryTabsSectionProps } from '../types';
|
import type { AgentCategoryTabsSectionProps } from '../types';
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ export default function AgentCategoryTabsSection({
|
|||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex-shrink-0 border-b border-border">
|
||||||
<div role="tablist" className="flex overflow-x-auto px-2 md:px-4">
|
<div role="tablist" className="flex overflow-x-auto px-2 md:px-4">
|
||||||
{AGENT_CATEGORIES.map((category) => (
|
{AGENT_CATEGORIES.map((category) => (
|
||||||
<button
|
<button
|
||||||
@@ -19,11 +20,12 @@ export default function AgentCategoryTabsSection({
|
|||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={selectedCategory === category}
|
aria-selected={selectedCategory === category}
|
||||||
onClick={() => onSelectCategory(category)}
|
onClick={() => onSelectCategory(category)}
|
||||||
className={`whitespace-nowrap border-b-2 px-3 py-2 text-xs font-medium transition-colors md:px-4 md:py-3 md:text-sm ${
|
className={cn(
|
||||||
|
'whitespace-nowrap border-b-2 px-4 py-3 text-sm font-medium touch-manipulation transition-colors duration-150',
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'border-blue-600 text-blue-600 dark:text-blue-400'
|
? 'border-primary text-primary'
|
||||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
: 'border-transparent text-muted-foreground hover:text-foreground',
|
||||||
}`}
|
)}
|
||||||
>
|
>
|
||||||
{category === 'account' && t('tabs.account')}
|
{category === 'account' && t('tabs.account')}
|
||||||
{category === 'permissions' && t('tabs.permissions')}
|
{category === 'permissions' && t('tabs.permissions')}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
import { PillBar, Pill } from '../../../../../../shared/view/ui';
|
||||||
|
import SessionProviderLogo from '../../../../../llm-logo-provider/SessionProviderLogo';
|
||||||
import type { AgentProvider } from '../../../../types/types';
|
import type { AgentProvider } from '../../../../types/types';
|
||||||
import AgentListItem from '../AgentListItem';
|
|
||||||
import type { AgentSelectorSectionProps } from '../types';
|
import type { AgentSelectorSectionProps } from '../types';
|
||||||
|
|
||||||
const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex'];
|
const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
|
||||||
|
|
||||||
|
const AGENT_NAMES: Record<AgentProvider, string> = {
|
||||||
|
claude: 'Claude',
|
||||||
|
cursor: 'Cursor',
|
||||||
|
codex: 'Codex',
|
||||||
|
gemini: 'Gemini',
|
||||||
|
};
|
||||||
|
|
||||||
export default function AgentSelectorSection({
|
export default function AgentSelectorSection({
|
||||||
selectedAgent,
|
selectedAgent,
|
||||||
@@ -10,35 +18,30 @@ export default function AgentSelectorSection({
|
|||||||
agentContextById,
|
agentContextById,
|
||||||
}: AgentSelectorSectionProps) {
|
}: AgentSelectorSectionProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex-shrink-0 border-b border-border px-3 py-2 md:px-4 md:py-3">
|
||||||
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 md:hidden">
|
<PillBar className="w-full md:w-auto">
|
||||||
<div className="flex">
|
{AGENT_PROVIDERS.map((agent) => {
|
||||||
{AGENT_PROVIDERS.map((agent) => (
|
const dotColor =
|
||||||
<AgentListItem
|
agent === 'claude' ? 'bg-blue-500' :
|
||||||
key={`mobile-${agent}`}
|
agent === 'cursor' ? 'bg-purple-500' :
|
||||||
agentId={agent}
|
agent === 'gemini' ? 'bg-indigo-500' : 'bg-foreground/60';
|
||||||
authStatus={agentContextById[agent].authStatus}
|
|
||||||
isSelected={selectedAgent === agent}
|
|
||||||
onClick={() => onSelectAgent(agent)}
|
|
||||||
isMobile
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden w-48 flex-shrink-0 border-r border-gray-200 dark:border-gray-700 md:block">
|
return (
|
||||||
<div className="p-2">
|
<Pill
|
||||||
{AGENT_PROVIDERS.map((agent) => (
|
key={agent}
|
||||||
<AgentListItem
|
isActive={selectedAgent === agent}
|
||||||
key={`desktop-${agent}`}
|
|
||||||
agentId={agent}
|
|
||||||
authStatus={agentContextById[agent].authStatus}
|
|
||||||
isSelected={selectedAgent === agent}
|
|
||||||
onClick={() => onSelectAgent(agent)}
|
onClick={() => onSelectAgent(agent)}
|
||||||
/>
|
className="min-w-0 flex-1 justify-center md:flex-initial"
|
||||||
))}
|
>
|
||||||
</div>
|
<SessionProviderLogo provider={agent} className="h-4 w-4 flex-shrink-0" />
|
||||||
</div>
|
<span className="truncate">{AGENT_NAMES[agent]}</span>
|
||||||
</>
|
{agentContextById[agent].authStatus.authenticated && (
|
||||||
|
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${dotColor}`} />
|
||||||
|
)}
|
||||||
|
</Pill>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PillBar>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
|||||||
borderClass: 'border-blue-200 dark:border-blue-800',
|
borderClass: 'border-blue-200 dark:border-blue-800',
|
||||||
textClass: 'text-blue-900 dark:text-blue-100',
|
textClass: 'text-blue-900 dark:text-blue-100',
|
||||||
subtextClass: 'text-blue-700 dark:text-blue-300',
|
subtextClass: 'text-blue-700 dark:text-blue-300',
|
||||||
buttonClass: 'bg-blue-600 hover:bg-blue-700',
|
buttonClass: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800',
|
||||||
},
|
},
|
||||||
cursor: {
|
cursor: {
|
||||||
name: 'Cursor',
|
name: 'Cursor',
|
||||||
@@ -35,15 +35,15 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
|||||||
borderClass: 'border-purple-200 dark:border-purple-800',
|
borderClass: 'border-purple-200 dark:border-purple-800',
|
||||||
textClass: 'text-purple-900 dark:text-purple-100',
|
textClass: 'text-purple-900 dark:text-purple-100',
|
||||||
subtextClass: 'text-purple-700 dark:text-purple-300',
|
subtextClass: 'text-purple-700 dark:text-purple-300',
|
||||||
buttonClass: 'bg-purple-600 hover:bg-purple-700',
|
buttonClass: 'bg-purple-600 hover:bg-purple-700 active:bg-purple-800',
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
name: 'Codex',
|
name: 'Codex',
|
||||||
bgClass: 'bg-gray-100 dark:bg-gray-800/50',
|
bgClass: 'bg-muted/50',
|
||||||
borderClass: 'border-gray-300 dark:border-gray-600',
|
borderClass: 'border-gray-300 dark:border-gray-600',
|
||||||
textClass: 'text-gray-900 dark:text-gray-100',
|
textClass: 'text-gray-900 dark:text-gray-100',
|
||||||
subtextClass: 'text-gray-700 dark:text-gray-300',
|
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',
|
buttonClass: 'bg-gray-800 hover:bg-gray-900 active:bg-gray-950 dark:bg-gray-700 dark:hover:bg-gray-600 dark:active:bg-gray-500',
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
name: 'Gemini',
|
name: 'Gemini',
|
||||||
@@ -52,7 +52,7 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
|||||||
borderClass: 'border-indigo-200 dark:border-indigo-800',
|
borderClass: 'border-indigo-200 dark:border-indigo-800',
|
||||||
textClass: 'text-indigo-900 dark:text-indigo-100',
|
textClass: 'text-indigo-900 dark:text-indigo-100',
|
||||||
subtextClass: 'text-indigo-700 dark:text-indigo-300',
|
subtextClass: 'text-indigo-700 dark:text-indigo-300',
|
||||||
buttonClass: 'bg-indigo-600 hover:bg-indigo-700',
|
buttonClass: 'bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{authStatus.loading ? (
|
{authStatus.loading ? (
|
||||||
<Badge variant="secondary" className="bg-gray-100 dark:bg-gray-800">
|
<Badge variant="secondary" className="bg-muted">
|
||||||
{t('agents.authStatus.checking')}
|
{t('agents.authStatus.checking')}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : authStatus.authenticated ? (
|
) : authStatus.authenticated ? (
|
||||||
@@ -107,7 +107,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{authStatus.method !== 'api_key' && (
|
{authStatus.method !== 'api_key' && (
|
||||||
<div className="border-t border-gray-200 pt-4 dark:border-gray-700">
|
<div className="border-t border-border/50 pt-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className={`font-medium ${config.textClass}`}>
|
<div className={`font-medium ${config.textClass}`}>
|
||||||
@@ -132,7 +132,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{authStatus.error && (
|
{authStatus.error && (
|
||||||
<div className="border-t border-gray-200 pt-4 dark:border-gray-700">
|
<div className="border-t border-border/50 pt-4">
|
||||||
<div className="text-sm text-red-600 dark:text-red-400">
|
<div className="text-sm text-red-600 dark:text-red-400">
|
||||||
{t('agents.error', { error: authStatus.error })}
|
{t('agents.error', { error: authStatus.error })}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function ClaudeMcpServers({
|
|||||||
const toolsResult = serverTools[serverId];
|
const toolsResult = serverTools[serverId];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={serverId} className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<div key={serverId} className="rounded-lg border border-border bg-card/50 p-4">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
@@ -102,19 +102,19 @@ function ClaudeMcpServers({
|
|||||||
{server.type === 'stdio' && server.config?.command && (
|
{server.type === 'stdio' && server.config?.command && (
|
||||||
<div>
|
<div>
|
||||||
{t('mcpServers.config.command')}:{' '}
|
{t('mcpServers.config.command')}:{' '}
|
||||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.command}</code>
|
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(server.type === 'sse' || server.type === 'http') && server.config?.url && (
|
{(server.type === 'sse' || server.type === 'http') && server.config?.url && (
|
||||||
<div>
|
<div>
|
||||||
{t('mcpServers.config.url')}:{' '}
|
{t('mcpServers.config.url')}:{' '}
|
||||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.url}</code>
|
<code className="rounded bg-muted px-1 text-xs">{server.config.url}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{server.config?.args && server.config.args.length > 0 && (
|
{server.config?.args && server.config.args.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
{t('mcpServers.config.args')}:{' '}
|
{t('mcpServers.config.args')}:{' '}
|
||||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.args.join(' ')}</code>
|
<code className="rounded bg-muted px-1 text-xs">{server.config.args.join(' ')}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +156,7 @@ function ClaudeMcpServers({
|
|||||||
onClick={() => onEdit(server)}
|
onClick={() => onEdit(server)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-gray-600 hover:text-gray-700"
|
className="text-muted-foreground hover:text-foreground"
|
||||||
title={t('mcpServers.actions.edit')}
|
title={t('mcpServers.actions.edit')}
|
||||||
>
|
>
|
||||||
<Edit3 className="h-4 w-4" />
|
<Edit3 className="h-4 w-4" />
|
||||||
@@ -176,7 +176,7 @@ function ClaudeMcpServers({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{servers.length === 0 && (
|
{servers.length === 0 && (
|
||||||
<div className="py-8 text-center text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
|
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +214,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
|||||||
const serverId = server.id || server.name;
|
const serverId = server.id || server.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={serverId} className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<div key={serverId} className="rounded-lg border border-border bg-card/50 p-4">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
@@ -226,7 +226,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
|||||||
{server.config?.command && (
|
{server.config?.command && (
|
||||||
<div>
|
<div>
|
||||||
{t('mcpServers.config.command')}:{' '}
|
{t('mcpServers.config.command')}:{' '}
|
||||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.command}</code>
|
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -236,7 +236,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
|||||||
onClick={() => onEdit(server)}
|
onClick={() => onEdit(server)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-gray-600 hover:text-gray-700"
|
className="text-muted-foreground hover:text-foreground"
|
||||||
title={t('mcpServers.actions.edit')}
|
title={t('mcpServers.actions.edit')}
|
||||||
>
|
>
|
||||||
<Edit3 className="h-4 w-4" />
|
<Edit3 className="h-4 w-4" />
|
||||||
@@ -256,7 +256,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{servers.length === 0 && (
|
{servers.length === 0 && (
|
||||||
<div className="py-8 text-center text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
|
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,7 +278,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Server className="h-5 w-5 text-gray-700 dark:text-gray-300" />
|
<Server className="h-5 w-5 text-muted-foreground" />
|
||||||
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
|
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{t('mcpServers.description.codex')}</p>
|
<p className="text-sm text-muted-foreground">{t('mcpServers.description.codex')}</p>
|
||||||
@@ -297,7 +297,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{servers.map((server) => (
|
{servers.map((server) => (
|
||||||
<div key={server.name} className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<div key={server.name} className="rounded-lg border border-border bg-card/50 p-4">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
@@ -310,19 +310,19 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
|||||||
{server.config?.command && (
|
{server.config?.command && (
|
||||||
<div>
|
<div>
|
||||||
{t('mcpServers.config.command')}:{' '}
|
{t('mcpServers.config.command')}:{' '}
|
||||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.command}</code>
|
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{server.config?.args && server.config.args.length > 0 && (
|
{server.config?.args && server.config.args.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
{t('mcpServers.config.args')}:{' '}
|
{t('mcpServers.config.args')}:{' '}
|
||||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.args.join(' ')}</code>
|
<code className="rounded bg-muted px-1 text-xs">{server.config.args.join(' ')}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{server.config?.env && Object.keys(server.config.env).length > 0 && (
|
{server.config?.env && Object.keys(server.config.env).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
{t('mcpServers.config.environment')}:{' '}
|
{t('mcpServers.config.environment')}:{' '}
|
||||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">
|
<code className="rounded bg-muted px-1 text-xs">
|
||||||
{Object.entries(server.config.env).map(([key, value]) => `${key}=${maskSecret(value)}`).join(', ')}
|
{Object.entries(server.config.env).map(([key, value]) => `${key}=${maskSecret(value)}`).join(', ')}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,7 +335,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
|||||||
onClick={() => onEdit(server)}
|
onClick={() => onEdit(server)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-gray-600 hover:text-gray-700"
|
className="text-muted-foreground hover:text-foreground"
|
||||||
title={t('mcpServers.actions.edit')}
|
title={t('mcpServers.actions.edit')}
|
||||||
>
|
>
|
||||||
<Edit3 className="h-4 w-4" />
|
<Edit3 className="h-4 w-4" />
|
||||||
@@ -354,13 +354,13 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{servers.length === 0 && (
|
{servers.length === 0 && (
|
||||||
<div className="py-8 text-center text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
|
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-300 bg-gray-100 p-4 dark:border-gray-600 dark:bg-gray-800/50">
|
<div className="rounded-lg border border-border bg-muted/50 p-4">
|
||||||
<h4 className="mb-2 font-medium text-gray-900 dark:text-gray-100">{t('mcpServers.help.title')}</h4>
|
<h4 className="mb-2 font-medium text-foreground">{t('mcpServers.help.title')}</h4>
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300">{t('mcpServers.help.description')}</p>
|
<p className="text-sm text-muted-foreground">{t('mcpServers.help.description')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function ClaudePermissions({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={skipPermissions}
|
checked={skipPermissions}
|
||||||
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
|
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
className="h-4 w-4 rounded border-input bg-card text-primary focus:ring-2 focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||||
@@ -150,7 +150,7 @@ function ClaudePermissions({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
{t('permissions.allowedTools.quickAdd')}
|
{t('permissions.allowedTools.quickAdd')}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -184,7 +184,7 @@ function ClaudePermissions({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{allowedTools.length === 0 && (
|
{allowedTools.length === 0 && (
|
||||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
<div className="py-6 text-center text-muted-foreground">
|
||||||
{t('permissions.allowedTools.empty')}
|
{t('permissions.allowedTools.empty')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -237,7 +237,7 @@ function ClaudePermissions({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{disallowedTools.length === 0 && (
|
{disallowedTools.length === 0 && (
|
||||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
<div className="py-6 text-center text-muted-foreground">
|
||||||
{t('permissions.blockedTools.empty')}
|
{t('permissions.blockedTools.empty')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -314,7 +314,7 @@ function CursorPermissions({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={skipPermissions}
|
checked={skipPermissions}
|
||||||
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
|
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-purple-600 focus:ring-2 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
|
className="h-4 w-4 rounded border-input bg-card text-primary focus:ring-2 focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||||
@@ -360,7 +360,7 @@ function CursorPermissions({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
{t('permissions.allowedCommands.quickAdd')}
|
{t('permissions.allowedCommands.quickAdd')}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -394,7 +394,7 @@ function CursorPermissions({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{allowedCommands.length === 0 && (
|
{allowedCommands.length === 0 && (
|
||||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
<div className="py-6 text-center text-muted-foreground">
|
||||||
{t('permissions.allowedCommands.empty')}
|
{t('permissions.allowedCommands.empty')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -447,7 +447,7 @@ function CursorPermissions({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{disallowedCommands.length === 0 && (
|
{disallowedCommands.length === 0 && (
|
||||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
<div className="py-6 text-center text-muted-foreground">
|
||||||
{t('permissions.blockedCommands.empty')}
|
{t('permissions.blockedCommands.empty')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -489,8 +489,8 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'default'
|
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'default'
|
||||||
? 'border-gray-400 bg-gray-100 dark:border-gray-500 dark:bg-gray-800'
|
? 'border-border bg-accent'
|
||||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onPermissionModeChange('default')}
|
onClick={() => onPermissionModeChange('default')}
|
||||||
>
|
>
|
||||||
@@ -514,7 +514,7 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
|||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'acceptEdits'
|
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'acceptEdits'
|
||||||
? 'border-green-400 bg-green-50 dark:border-green-600 dark:bg-green-900/20'
|
? 'border-green-400 bg-green-50 dark:border-green-600 dark:bg-green-900/20'
|
||||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onPermissionModeChange('acceptEdits')}
|
onClick={() => onPermissionModeChange('acceptEdits')}
|
||||||
>
|
>
|
||||||
@@ -538,7 +538,7 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
|||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'bypassPermissions'
|
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'bypassPermissions'
|
||||||
? 'border-orange-400 bg-orange-50 dark:border-orange-600 dark:bg-orange-900/20'
|
? 'border-orange-400 bg-orange-50 dark:border-orange-600 dark:bg-orange-900/20'
|
||||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onPermissionModeChange('bypassPermissions')}
|
onClick={() => onPermissionModeChange('bypassPermissions')}
|
||||||
>
|
>
|
||||||
@@ -566,7 +566,7 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
|||||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
||||||
{t('permissions.codex.technicalDetails')}
|
{t('permissions.codex.technicalDetails')}
|
||||||
</summary>
|
</summary>
|
||||||
<div className="mt-2 space-y-2 rounded-lg bg-gray-50 p-3 text-xs text-muted-foreground dark:bg-gray-900/50">
|
<div className="mt-2 space-y-2 rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground">
|
||||||
<p><strong>{t('permissions.codex.modes.default.title')}:</strong> {t('permissions.codex.technicalInfo.default')}</p>
|
<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.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><strong>{t('permissions.codex.modes.bypassPermissions.title')}:</strong> {t('permissions.codex.technicalInfo.bypassPermissions')}</p>
|
||||||
@@ -603,8 +603,8 @@ function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<Gemi
|
|||||||
{/* Default Mode */}
|
{/* Default Mode */}
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'default'
|
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'default'
|
||||||
? 'border-gray-400 bg-gray-100 dark:border-gray-500 dark:bg-gray-800'
|
? 'border-border bg-accent'
|
||||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onPermissionModeChange('default')}
|
onClick={() => onPermissionModeChange('default')}
|
||||||
>
|
>
|
||||||
@@ -629,7 +629,7 @@ function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<Gemi
|
|||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'auto_edit'
|
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'auto_edit'
|
||||||
? 'border-green-400 bg-green-50 dark:border-green-600 dark:bg-green-900/20'
|
? 'border-green-400 bg-green-50 dark:border-green-600 dark:bg-green-900/20'
|
||||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onPermissionModeChange('auto_edit')}
|
onClick={() => onPermissionModeChange('auto_edit')}
|
||||||
>
|
>
|
||||||
@@ -654,7 +654,7 @@ function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<Gemi
|
|||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'yolo'
|
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'yolo'
|
||||||
? 'border-orange-400 bg-orange-50 dark:border-orange-600 dark:bg-orange-900/20'
|
? 'border-orange-400 bg-orange-50 dark:border-orange-600 dark:bg-orange-900/20'
|
||||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onPermissionModeChange('yolo')}
|
onClick={() => onPermissionModeChange('yolo')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Check, GitBranch } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useGitSettings } from '../../../hooks/useGitSettings';
|
import { useGitSettings } from '../../../hooks/useGitSettings';
|
||||||
import { Button, Input } from '../../../../../shared/view/ui';
|
import { Button, Input } from '../../../../../shared/view/ui';
|
||||||
|
import SettingsCard from '../../SettingsCard';
|
||||||
|
import SettingsSection from '../../SettingsSection';
|
||||||
|
|
||||||
export default function GitSettingsTab() {
|
export default function GitSettingsTab() {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
@@ -18,64 +20,62 @@ export default function GitSettingsTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<SettingsSection
|
||||||
<div className="mb-4 flex items-center gap-2">
|
title={t('git.title')}
|
||||||
<GitBranch className="h-5 w-5" />
|
description={t('git.description')}
|
||||||
<h3 className="text-lg font-semibold">{t('git.title')}</h3>
|
>
|
||||||
</div>
|
<SettingsCard className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="settings-git-name" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
|
{t('git.name.label')}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="settings-git-name"
|
||||||
|
type="text"
|
||||||
|
value={gitName}
|
||||||
|
onChange={(event) => setGitName(event.target.value)}
|
||||||
|
placeholder="John Doe"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">{t('git.name.help')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="mb-4 text-sm text-muted-foreground">{t('git.description')}</p>
|
<div>
|
||||||
|
<label htmlFor="settings-git-email" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
|
{t('git.email.label')}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="settings-git-email"
|
||||||
|
type="email"
|
||||||
|
value={gitEmail}
|
||||||
|
onChange={(event) => setGitEmail(event.target.value)}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">{t('git.email.help')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 rounded-lg border bg-card p-4">
|
<div className="flex items-center gap-2">
|
||||||
<div>
|
<Button
|
||||||
<label htmlFor="settings-git-name" className="mb-2 block text-sm font-medium text-foreground">
|
onClick={saveGitConfig}
|
||||||
{t('git.name.label')}
|
disabled={isSaving || !gitName.trim() || !gitEmail.trim()}
|
||||||
</label>
|
>
|
||||||
<Input
|
{isSaving ? t('git.actions.saving') : t('git.actions.save')}
|
||||||
id="settings-git-name"
|
</Button>
|
||||||
type="text"
|
|
||||||
value={gitName}
|
{saveStatus === 'success' && (
|
||||||
onChange={(event) => setGitName(event.target.value)}
|
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||||
placeholder="John Doe"
|
<Check className="h-4 w-4" />
|
||||||
disabled={isLoading}
|
{t('git.status.success')}
|
||||||
className="w-full"
|
</div>
|
||||||
/>
|
)}
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{t('git.name.help')}</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
<div>
|
</SettingsSection>
|
||||||
<label htmlFor="settings-git-email" className="mb-2 block text-sm font-medium text-foreground">
|
|
||||||
{t('git.email.label')}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="settings-git-email"
|
|
||||||
type="email"
|
|
||||||
value={gitEmail}
|
|
||||||
onChange={(event) => setGitEmail(event.target.value)}
|
|
||||||
placeholder="john@example.com"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{t('git.email.help')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={saveGitConfig}
|
|
||||||
disabled={isSaving || !gitName.trim() || !gitEmail.trim()}
|
|
||||||
>
|
|
||||||
{isSaving ? t('git.actions.saving') : t('git.actions.save')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{saveStatus === 'success' && (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
{t('git.status.success')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTasksSettings } from '../../../../../contexts/TasksSettingsContext';
|
import { useTasksSettings } from '../../../../../contexts/TasksSettingsContext';
|
||||||
|
import SettingsCard from '../../SettingsCard';
|
||||||
|
import SettingsRow from '../../SettingsRow';
|
||||||
|
import SettingsSection from '../../SettingsSection';
|
||||||
|
import SettingsToggle from '../../SettingsToggle';
|
||||||
|
|
||||||
type TasksSettingsContextValue = {
|
type TasksSettingsContextValue = {
|
||||||
tasksEnabled: boolean;
|
tasksEnabled: boolean;
|
||||||
@@ -19,88 +23,83 @@ export default function TasksSettingsTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{isCheckingInstallation ? (
|
<SettingsSection title={t('mainTabs.tasks')}>
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
{isCheckingInstallation ? (
|
||||||
<div className="flex items-center gap-3">
|
<SettingsCard className="p-4">
|
||||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" />
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm text-muted-foreground">{t('tasks.checking')}</span>
|
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||||
</div>
|
<span className="text-sm text-muted-foreground">{t('tasks.checking')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</SettingsCard>
|
||||||
<>
|
) : (
|
||||||
{!isTaskMasterInstalled && (
|
<>
|
||||||
<div className="rounded-lg border border-orange-200 bg-orange-50 p-4 dark:border-orange-800 dark:bg-orange-950/50">
|
{!isTaskMasterInstalled && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="rounded-xl border border-orange-200 bg-orange-50 p-4 dark:border-orange-800/50 dark:bg-orange-950/30">
|
||||||
<div className="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-orange-100 dark:bg-orange-900">
|
<div className="flex items-start gap-3">
|
||||||
<svg className="h-4 w-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-orange-100 dark:bg-orange-900/50">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
<svg className="h-4 w-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
</div>
|
</svg>
|
||||||
<div className="flex-1">
|
|
||||||
<div className="mb-2 font-medium text-orange-900 dark:text-orange-100">
|
|
||||||
{t('tasks.notInstalled.title')}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 text-sm text-orange-800 dark:text-orange-200">
|
<div className="flex-1">
|
||||||
<p>{t('tasks.notInstalled.description')}</p>
|
<div className="mb-2 font-medium text-orange-900 dark:text-orange-100">
|
||||||
|
{t('tasks.notInstalled.title')}
|
||||||
<div className="rounded-lg bg-orange-100 p-3 font-mono text-sm dark:bg-orange-900/50">
|
|
||||||
<code>{t('tasks.notInstalled.installCommand')}</code>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-3 text-sm text-orange-800 dark:text-orange-200">
|
||||||
|
<p>{t('tasks.notInstalled.description')}</p>
|
||||||
|
|
||||||
<div>
|
<div className="rounded-lg bg-orange-100 p-3 font-mono text-sm dark:bg-orange-900/40">
|
||||||
<a
|
<code>{t('tasks.notInstalled.installCommand')}</code>
|
||||||
href="https://github.com/eyaltoledano/claude-task-master"
|
</div>
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
||||||
>
|
|
||||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{t('tasks.notInstalled.viewOnGitHub')}
|
|
||||||
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div>
|
||||||
<p className="font-medium">{t('tasks.notInstalled.afterInstallation')}</p>
|
<a
|
||||||
<ol className="list-inside list-decimal space-y-1 text-xs">
|
href="https://github.com/eyaltoledano/claude-task-master"
|
||||||
<li>{t('tasks.notInstalled.steps.restart')}</li>
|
target="_blank"
|
||||||
<li>{t('tasks.notInstalled.steps.autoAvailable')}</li>
|
rel="noopener noreferrer"
|
||||||
<li>{t('tasks.notInstalled.steps.initCommand')}</li>
|
className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
</ol>
|
>
|
||||||
|
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
{t('tasks.notInstalled.viewOnGitHub')}
|
||||||
|
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-medium">{t('tasks.notInstalled.afterInstallation')}</p>
|
||||||
|
<ol className="list-inside list-decimal space-y-1 text-xs">
|
||||||
|
<li>{t('tasks.notInstalled.steps.restart')}</li>
|
||||||
|
<li>{t('tasks.notInstalled.steps.autoAvailable')}</li>
|
||||||
|
<li>{t('tasks.notInstalled.steps.initCommand')}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{isTaskMasterInstalled && (
|
{isTaskMasterInstalled && (
|
||||||
<div className="space-y-4">
|
<SettingsCard>
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<SettingsRow
|
||||||
<div className="flex items-center justify-between">
|
label={t('tasks.settings.enableLabel')}
|
||||||
<div>
|
description={t('tasks.settings.enableDescription')}
|
||||||
<div className="font-medium text-foreground">{t('tasks.settings.enableLabel')}</div>
|
>
|
||||||
<div className="mt-1 text-sm text-muted-foreground">{t('tasks.settings.enableDescription')}</div>
|
<SettingsToggle
|
||||||
</div>
|
checked={tasksEnabled}
|
||||||
<label className="relative inline-flex cursor-pointer items-center">
|
onChange={setTasksEnabled}
|
||||||
<input
|
ariaLabel={t('tasks.settings.enableLabel')}
|
||||||
type="checkbox"
|
/>
|
||||||
checked={tasksEnabled}
|
</SettingsRow>
|
||||||
onChange={(event) => setTasksEnabled(event.target.checked)}
|
</SettingsCard>
|
||||||
className="peer sr-only"
|
)}
|
||||||
/>
|
</>
|
||||||
<div className="peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800" />
|
)}
|
||||||
</label>
|
</SettingsSection>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,6 +309,9 @@
|
|||||||
},
|
},
|
||||||
"codex": {
|
"codex": {
|
||||||
"description": "OpenAI Codex AI assistant"
|
"description": "OpenAI Codex AI assistant"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"description": "Google Gemini AI assistant"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connectionStatus": "Connection Status",
|
"connectionStatus": "Connection Status",
|
||||||
|
|||||||
@@ -309,6 +309,9 @@
|
|||||||
},
|
},
|
||||||
"codex": {
|
"codex": {
|
||||||
"description": "OpenAI Codex AIアシスタント"
|
"description": "OpenAI Codex AIアシスタント"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"description": "Google Gemini AIアシスタント"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connectionStatus": "接続状態",
|
"connectionStatus": "接続状態",
|
||||||
|
|||||||
@@ -309,6 +309,9 @@
|
|||||||
},
|
},
|
||||||
"codex": {
|
"codex": {
|
||||||
"description": "OpenAI Codex AI 어시스턴트"
|
"description": "OpenAI Codex AI 어시스턴트"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"description": "Google Gemini AI 어시스턴트"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connectionStatus": "연결 상태",
|
"connectionStatus": "연결 상태",
|
||||||
|
|||||||
@@ -308,6 +308,9 @@
|
|||||||
},
|
},
|
||||||
"codex": {
|
"codex": {
|
||||||
"description": "AI-ассистент OpenAI Codex"
|
"description": "AI-ассистент OpenAI Codex"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"description": "AI-ассистент Google Gemini"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connectionStatus": "Статус подключения",
|
"connectionStatus": "Статус подключения",
|
||||||
|
|||||||
@@ -309,6 +309,9 @@
|
|||||||
},
|
},
|
||||||
"codex": {
|
"codex": {
|
||||||
"description": "OpenAI Codex AI 助手"
|
"description": "OpenAI Codex AI 助手"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"description": "Google Gemini AI 助手"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connectionStatus": "连接状态",
|
"connectionStatus": "连接状态",
|
||||||
|
|||||||
@@ -905,6 +905,16 @@
|
|||||||
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings content fade-in transition */
|
||||||
|
@keyframes settings-fade-in {
|
||||||
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content-enter {
|
||||||
|
animation: settings-fade-in 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
/* Search result highlight flash */
|
/* Search result highlight flash */
|
||||||
.search-highlight-flash {
|
.search-highlight-flash {
|
||||||
animation: search-flash 4s ease-out;
|
animation: search-flash 4s ease-out;
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ import { cn } from '../../../lib/utils';
|
|||||||
|
|
||||||
// Keep visual variants centralized so all button usages stay consistent.
|
// Keep visual variants centralized so all button usages stay consistent.
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium touch-manipulation transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90 active:bg-primary/80',
|
||||||
destructive:
|
destructive:
|
||||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 active:bg-destructive/80',
|
||||||
outline:
|
outline:
|
||||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground active:bg-accent/80',
|
||||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 active:bg-secondary/70',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground active:bg-accent/80',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-9 px-4 py-2',
|
default: 'h-10 px-4 py-2',
|
||||||
sm: 'h-8 rounded-md px-3 text-xs',
|
sm: 'h-9 rounded-md px-3 text-sm',
|
||||||
lg: 'h-10 rounded-md px-8',
|
lg: 'h-11 rounded-md px-8',
|
||||||
icon: 'h-9 w-9',
|
icon: 'h-10 w-10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Moon, Sun } from 'lucide-react';
|
import { Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from '../../../contexts/ThemeContext';
|
import { useTheme } from '../../../contexts/ThemeContext';
|
||||||
|
import { cn } from '../../../lib/utils';
|
||||||
|
|
||||||
type DarkModeToggleProps = {
|
type DarkModeToggleProps = {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
@@ -13,7 +14,6 @@ function DarkModeToggle({
|
|||||||
ariaLabel = 'Toggle dark mode',
|
ariaLabel = 'Toggle dark mode',
|
||||||
}: DarkModeToggleProps) {
|
}: DarkModeToggleProps) {
|
||||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||||
// Support controlled usage while keeping ThemeContext as the default source of truth.
|
|
||||||
const isControlled = typeof checked === 'boolean' && typeof onToggle === 'function';
|
const isControlled = typeof checked === 'boolean' && typeof onToggle === 'function';
|
||||||
const isEnabled = isControlled ? checked : isDarkMode;
|
const isEnabled = isControlled ? checked : isDarkMode;
|
||||||
|
|
||||||
@@ -29,21 +29,26 @@ function DarkModeToggle({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-900"
|
className={cn(
|
||||||
|
'relative inline-flex h-7 w-12 flex-shrink-0 touch-manipulation cursor-pointer items-center rounded-full border-2 transition-colors duration-200',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
||||||
|
isEnabled ? 'border-primary bg-primary' : 'border-border bg-muted',
|
||||||
|
)}
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked={isEnabled}
|
aria-checked={isEnabled}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{ariaLabel}</span>
|
<span className="sr-only">{ariaLabel}</span>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={cn(
|
||||||
isEnabled ? 'translate-x-7' : 'translate-x-1'
|
'flex h-5 w-5 transform items-center justify-center rounded-full shadow-sm transition-transform duration-200',
|
||||||
} flex h-6 w-6 transform items-center justify-center rounded-full bg-white shadow-lg transition-transform duration-200`}
|
isEnabled ? 'translate-x-[22px] bg-white' : 'translate-x-[2px] bg-foreground/60 dark:bg-foreground/80',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isEnabled ? (
|
{isEnabled ? (
|
||||||
<Moon className="h-3.5 w-3.5 text-gray-700" />
|
<Moon className="h-3 w-3 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<Sun className="h-3.5 w-3.5 text-yellow-500" />
|
<Sun className="h-3 w-3 text-white dark:text-background" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ export default function LanguageSelector({ compact = false }: LanguageSelectorPr
|
|||||||
// Compact style for QuickSettingsPanel
|
// Compact style for QuickSettingsPanel
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between rounded-lg border border-transparent bg-gray-50 p-3 transition-colors hover:border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:hover:border-gray-600 dark:hover:bg-gray-700">
|
<div className="flex items-center justify-between rounded-lg border border-transparent bg-muted/50 p-3 transition-colors hover:border-border hover:bg-accent">
|
||||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
<span className="flex items-center gap-2 text-sm text-foreground">
|
||||||
<Languages className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
<Languages className="h-4 w-4 text-muted-foreground" />
|
||||||
{t('account.language')}
|
{t('account.language')}
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={i18n.language}
|
value={i18n.language}
|
||||||
onChange={handleLanguageChange}
|
onChange={handleLanguageChange}
|
||||||
className="w-[100px] rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:ring-blue-400"
|
className="w-[100px] rounded-lg border border-input bg-card p-2 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
>
|
>
|
||||||
{languages.map((lang) => (
|
{languages.map((lang) => (
|
||||||
<option key={lang.value} value={lang.value}>
|
<option key={lang.value} value={lang.value}>
|
||||||
@@ -50,28 +50,26 @@ export default function LanguageSelector({ compact = false }: LanguageSelectorPr
|
|||||||
|
|
||||||
// Full style for Settings page
|
// Full style for Settings page
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
<div className="flex items-center justify-between px-4 py-3.5">
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<div>
|
<div className="text-sm font-medium text-foreground">
|
||||||
<div className="mb-1 font-medium text-gray-900 dark:text-gray-100">
|
{t('account.languageLabel')}
|
||||||
{t('account.languageLabel')}
|
</div>
|
||||||
</div>
|
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
{t('account.languageDescription')}
|
||||||
{t('account.languageDescription')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<select
|
|
||||||
value={i18n.language}
|
|
||||||
onChange={handleLanguageChange}
|
|
||||||
className="w-36 rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
|
||||||
>
|
|
||||||
{languages.map((lang) => (
|
|
||||||
<option key={lang.value} value={lang.value}>
|
|
||||||
{lang.nativeName}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
<select
|
||||||
|
value={i18n.language}
|
||||||
|
onChange={handleLanguageChange}
|
||||||
|
className="w-36 rounded-lg border border-input bg-card p-2 text-sm text-foreground focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
|
>
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<option key={lang.value} value={lang.value}>
|
||||||
|
{lang.nativeName}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/shared/view/ui/PillBar.tsx
Normal file
41
src/shared/view/ui/PillBar.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { cn } from '../../../lib/utils';
|
||||||
|
|
||||||
|
/* ── Container ─────────────────────────────────────────────────── */
|
||||||
|
type PillBarProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PillBar({ children, className }: PillBarProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('inline-flex items-center gap-[2px] rounded-lg bg-muted/60 p-[3px]', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Individual pill button ────────────────────────────────────── */
|
||||||
|
type PillProps = {
|
||||||
|
isActive: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Pill({ isActive, onClick, children, className }: PillProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
'flex touch-manipulation items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-all duration-150',
|
||||||
|
isActive
|
||||||
|
? 'bg-background text-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground active:bg-background/50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ export { default as DarkModeToggle } from './DarkModeToggle';
|
|||||||
export { Input } from './Input';
|
export { Input } from './Input';
|
||||||
export { ScrollArea } from './ScrollArea';
|
export { ScrollArea } from './ScrollArea';
|
||||||
export { default as Tooltip } from './Tooltip';
|
export { default as Tooltip } from './Tooltip';
|
||||||
|
export { PillBar, Pill } from './PillBar';
|
||||||
|
|||||||
Reference in New Issue
Block a user