This commit is contained in:
2025-08-06 15:39:05 +08:00
parent 351a79d54c
commit 6798be3b42
26 changed files with 1243 additions and 469 deletions

View File

@@ -30,6 +30,7 @@ impl SimpleI18n {
}
}
#[allow(dead_code)]
pub fn t(&self, key: &str) -> String {
let locale = self.get_current_locale();
@@ -63,6 +64,7 @@ fn get_i18n() -> &'static SimpleI18n {
}
// 便捷函数用于全局访问
#[allow(dead_code)]
pub fn t(key: &str) -> String {
get_i18n().t(key)
}

View File

@@ -27,6 +27,7 @@ import { AgentsModal } from "@/components/AgentsModal";
import { useTabState } from "@/hooks/useTabState";
import { AnalyticsConsentBanner } from "@/components/AnalyticsConsent";
import { useAppLifecycle, useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
type View =
| "welcome"
@@ -48,6 +49,7 @@ type View =
* AppContent component - Contains the main app logic, wrapped by providers
*/
function AppContent() {
const { t } = useTranslation();
const [view, setView] = useState<View>("tabs");
const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState();
const [projects, setProjects] = useState<Project[]>([]);
@@ -158,7 +160,7 @@ function AppContent() {
setProjects(projectList);
} catch (err) {
console.error("Failed to load projects:", err);
setError("Failed to load projects. Please ensure ~/.claude directory exists.");
setError(t('failedToLoadProjects'));
} finally {
setLoading(false);
}
@@ -176,7 +178,7 @@ function AppContent() {
setSelectedProject(project);
} catch (err) {
console.error("Failed to load sessions:", err);
setError("Failed to load sessions for this project.");
setError(t('failedToLoadSessions'));
} finally {
setLoading(false);
}
@@ -246,7 +248,7 @@ function AppContent() {
>
<h1 className="text-4xl font-bold tracking-tight">
<span className="rotating-symbol"></span>
Welcome to Claudia
{t('welcomeToClaudia')}
</h1>
</motion.div>
@@ -264,7 +266,7 @@ function AppContent() {
>
<div className="h-full flex flex-col items-center justify-center p-8">
<Bot className="h-16 w-16 mb-4 text-primary" />
<h2 className="text-xl font-semibold">CC Agents</h2>
<h2 className="text-xl font-semibold">{t('ccAgents')}</h2>
</div>
</Card>
</motion.div>
@@ -281,7 +283,7 @@ function AppContent() {
>
<div className="h-full flex flex-col items-center justify-center p-8">
<FolderCode className="h-16 w-16 mb-4 text-primary" />
<h2 className="text-xl font-semibold">CC Projects</h2>
<h2 className="text-xl font-semibold">{t('ccProjects')}</h2>
</div>
</Card>
</motion.div>
@@ -329,12 +331,12 @@ function AppContent() {
onClick={() => handleViewChange("welcome")}
className="mb-4"
>
Back to Home
{t('backToHome')}
</Button>
<div className="mb-4">
<h1 className="text-3xl font-bold tracking-tight">CC Projects</h1>
<h1 className="text-3xl font-bold tracking-tight">{t('ccProjects')}</h1>
<p className="mt-1 text-sm text-muted-foreground">
Browse your Claude Code sessions
{t('browseClaudeCodeSessions')}
</p>
</div>
</motion.div>
@@ -396,7 +398,7 @@ function AppContent() {
className="w-full max-w-md"
>
<Plus className="mr-2 h-4 w-4" />
New Claude Code session
{t('newClaudeCodeSession')}
</Button>
</motion.div>
@@ -415,7 +417,7 @@ function AppContent() {
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">
No projects found in ~/.claude/projects
{t('noProjectsFound')}
</p>
</div>
)}
@@ -508,7 +510,7 @@ function AppContent() {
open={showClaudeBinaryDialog}
onOpenChange={setShowClaudeBinaryDialog}
onSuccess={() => {
setToast({ message: "Claude binary path saved successfully", type: "success" });
setToast({ message: t('claudeBinaryPathSaved'), type: "success" });
// Trigger a refresh of the Claude version check
window.location.reload();
}}

View File

@@ -21,6 +21,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Toast } from '@/components/ui/toast';
import { api, type Agent, type AgentRunWithMetrics } from '@/lib/api';
import { useTabState } from '@/hooks/useTabState';
import { useTranslation } from '@/hooks/useTranslation';
import { formatISOTimestamp } from '@/lib/date-utils';
import { open as openDialog, save } from '@tauri-apps/plugin-dialog';
import { invoke } from '@tauri-apps/api/core';
@@ -32,6 +33,7 @@ interface AgentsModalProps {
}
export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange }) => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('agents');
const [agents, setAgents] = useState<Agent[]>([]);
const [runningAgents, setRunningAgents] = useState<AgentRunWithMetrics[]>([]);
@@ -201,18 +203,18 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
<DialogHeader className="px-6 pt-6">
<DialogTitle className="flex items-center gap-2">
<Bot className="w-5 h-5" />
Agent Management
{t('agents.agentManagement')}
</DialogTitle>
<DialogDescription>
Create new agents or manage running agent executions
{t('agents.createNewOrManageAgents')}
</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
<TabsList className="mx-6">
<TabsTrigger value="agents">Available Agents</TabsTrigger>
<TabsTrigger value="agents">{t('agents.availableAgents')}</TabsTrigger>
<TabsTrigger value="running" className="relative">
Running Agents
{t('agents.runningAgents')}
{runningAgents.length > 0 && (
<Badge variant="secondary" className="ml-2 h-5 px-1.5">
{runningAgents.length}
@@ -257,9 +259,9 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
) : agents.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<Bot className="w-12 h-12 text-muted-foreground mb-4" />
<p className="text-lg font-medium mb-2">No agents available</p>
<p className="text-lg font-medium mb-2">{t('agents.noAgentsAvailable')}</p>
<p className="text-sm text-muted-foreground mb-4">
Create your first agent to get started
{t('agents.createFirstAgentToGetStarted')}
</p>
<Button onClick={() => {
onOpenChange(false);

View File

@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { analytics } from '@/lib/analytics';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/hooks/useTranslation';
interface AnalyticsConsentProps {
open?: boolean;
@@ -18,6 +19,7 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
onOpenChange,
onComplete,
}) => {
const { t } = useTranslation();
const [internalOpen, setInternalOpen] = useState(false);
const [hasShownConsent, setHasShownConsent] = useState(false);
@@ -70,10 +72,10 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
<div className="p-2 bg-purple-100 dark:bg-purple-900/20 rounded-lg">
<BarChart3 className="h-6 w-6 text-purple-600 dark:text-purple-400" />
</div>
<DialogTitle className="text-2xl">Help Improve Claudia</DialogTitle>
<DialogTitle className="text-2xl">{t('analytics.helpImproveClaudia')}</DialogTitle>
</div>
<DialogDescription className="text-base mt-2">
We'd like to collect anonymous usage data to improve your experience.
{t('analytics.collectAnonymousData')}
</DialogDescription>
</DialogHeader>
</div>
@@ -84,12 +86,12 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
<div className="flex gap-3">
<Check className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="font-medium text-green-900 dark:text-green-100">What we collect:</p>
<p className="font-medium text-green-900 dark:text-green-100">{t('analytics.whatWeCollect')}</p>
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
<li>• Feature usage (which tools and commands you use)</li>
<li>• Performance metrics (app speed and reliability)</li>
<li>• Error reports (to fix bugs and improve stability)</li>
<li>• General usage patterns (session frequency and duration)</li>
<li> {t('analytics.featureUsageDesc')}</li>
<li> {t('analytics.performanceMetricsDesc')}</li>
<li> {t('analytics.errorReportsDesc')}</li>
<li> {t('analytics.usagePatternsDesc')}</li>
</ul>
</div>
</div>
@@ -99,13 +101,13 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
<div className="flex gap-3">
<Shield className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="font-medium text-blue-900 dark:text-blue-100">Your privacy is protected:</p>
<p className="font-medium text-blue-900 dark:text-blue-100">{t('analytics.privacyProtected')}</p>
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
<li>• No personal information is collected</li>
<li>• No file contents, paths, or project names</li>
<li>• No API keys or sensitive data</li>
<li>• Completely anonymous with random IDs</li>
<li>• You can opt-out anytime in Settings</li>
<li> {t('analytics.noPersonalInfo')}</li>
<li> {t('analytics.noFileContents')}</li>
<li> {t('analytics.noApiKeys')}</li>
<li> {t('analytics.anonymousData')}</li>
<li> {t('analytics.canOptOut')}</li>
</ul>
</div>
</div>
@@ -116,8 +118,7 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
<div className="flex gap-2 items-start">
<Info className="h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-gray-600 dark:text-gray-400">
This data helps us understand which features are most valuable, identify performance
issues, and prioritize improvements. Your choice won't affect any functionality.
{t('analytics.dataHelpsUs')}
</p>
</div>
</div>
@@ -129,13 +130,13 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
variant="outline"
className="flex-1"
>
No Thanks
{t('analytics.noThanks')}
</Button>
<Button
onClick={handleAccept}
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
>
Allow Analytics
{t('analytics.allowAnalytics')}
</Button>
</div>
</DialogContent>

View File

@@ -40,6 +40,7 @@ import { AgentExecution } from "./AgentExecution";
import { AgentRunsList } from "./AgentRunsList";
import { GitHubAgentBrowser } from "./GitHubAgentBrowser";
import { ICON_MAP } from "./IconPicker";
import { useTranslation } from "@/hooks/useTranslation";
interface CCAgentsProps {
/**
@@ -64,6 +65,7 @@ export type AgentIconName = keyof typeof AGENT_ICONS;
* <CCAgents onBack={() => setView('home')} />
*/
export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
const { t } = useTranslation();
const [agents, setAgents] = useState<Agent[]>([]);
const [runs, setRuns] = useState<AgentRunWithMetrics[]>([]);
const [loading, setLoading] = useState(true);
@@ -94,8 +96,8 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
setAgents(agentsList);
} catch (err) {
console.error("Failed to load agents:", err);
setError("Failed to load agents");
setToast({ message: "Failed to load agents", type: "error" });
setError(t('agents.loadAgentsFailed'));
setToast({ message: t('agents.loadAgentsFailed'), type: "error" });
} finally {
setLoading(false);
}
@@ -132,12 +134,12 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
try {
setIsDeleting(true);
await api.deleteAgent(agentToDelete.id);
setToast({ message: "Agent deleted successfully", type: "success" });
setToast({ message: t('agents.agentDeleted'), type: "success" });
await loadAgents();
await loadRuns(); // Reload runs as they might be affected
} catch (err) {
console.error("Failed to delete agent:", err);
setToast({ message: "Failed to delete agent", type: "error" });
setToast({ message: t('agents.deleteFailed'), type: "error" });
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
@@ -166,13 +168,13 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
const handleAgentCreated = async () => {
setView("list");
await loadAgents();
setToast({ message: "Agent created successfully", type: "success" });
setToast({ message: t('agents.agentCreated'), type: "success" });
};
const handleAgentUpdated = async () => {
setView("list");
await loadAgents();
setToast({ message: "Agent updated successfully", type: "success" });
setToast({ message: t('agents.agentUpdated'), type: "success" });
};
// const handleRunClick = (run: AgentRunWithMetrics) => {
@@ -209,10 +211,10 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
filePath
});
setToast({ message: `Agent "${agent.name}" exported successfully`, type: "success" });
setToast({ message: t('agents.exportedSuccessfully', { name: agent.name }), type: "success" });
} catch (err) {
console.error("Failed to export agent:", err);
setToast({ message: "Failed to export agent", type: "error" });
setToast({ message: t('agents.exportFailed'), type: "error" });
}
};
@@ -235,11 +237,11 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
// Import the agent from the selected file
await api.importAgentFromFile(filePath as string);
setToast({ message: "Agent imported successfully", type: "success" });
setToast({ message: t('agents.importedSuccessfully'), type: "success" });
await loadAgents();
} catch (err) {
console.error("Failed to import agent:", err);
const errorMessage = err instanceof Error ? err.message : "Failed to import agent";
const errorMessage = err instanceof Error ? err.message : t('agents.importFailed');
setToast({ message: errorMessage, type: "error" });
}
};
@@ -308,9 +310,9 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold">CC Agents</h1>
<h1 className="text-2xl font-bold">{t('navigation.agents')}</h1>
<p className="text-sm text-muted-foreground">
Manage your Claude Code agents
{t('agents.manageAgents')}
</p>
</div>
</div>
@@ -323,18 +325,18 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
Import
{t('agents.import')}
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleImportAgent}>
<FileJson className="h-4 w-4 mr-2" />
From File
{t('agents.importFromFile')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setShowGitHubBrowser(true)}>
<Globe className="h-4 w-4 mr-2" />
From GitHub
{t('agents.importFromGitHub')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -344,7 +346,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Create CC Agent
{t('agents.createAgent')}
</Button>
</div>
</div>
@@ -381,13 +383,13 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
) : agents.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-center">
<Bot className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">No agents yet</h3>
<h3 className="text-lg font-medium mb-2">{t('agents.noAgentsYet')}</h3>
<p className="text-sm text-muted-foreground mb-4">
Create your first CC Agent to get started
{t('agents.createFirstAgent')}
</p>
<Button onClick={() => setView("create")} size="default">
<Plus className="h-4 w-4 mr-2" />
Create CC Agent
{t('agents.createAgent')}
</Button>
</div>
) : (
@@ -411,7 +413,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
{agent.name}
</h3>
<p className="text-xs text-muted-foreground">
Created: {new Date(agent.created_at).toLocaleDateString()}
{t('agents.created')}: {new Date(agent.created_at).toLocaleDateString()}
</p>
</CardContent>
<CardFooter className="p-4 pt-0 flex justify-center gap-1 flex-wrap">
@@ -420,20 +422,20 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
variant="ghost"
onClick={() => handleExecuteAgent(agent)}
className="flex items-center gap-1"
title="Execute agent"
title={t('agents.executeAgentTitle')}
>
<Play className="h-3 w-3" />
Execute
{t('agents.execute')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleEditAgent(agent)}
className="flex items-center gap-1"
title="Edit agent"
title={t('agents.editAgent')}
>
<Edit className="h-3 w-3" />
Edit
{t('app.edit')}
</Button>
<Button
size="sm"
@@ -443,17 +445,17 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
title="Export agent to .claudia.json"
>
<Upload className="h-3 w-3" />
Export
{t('agents.export')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteAgent(agent)}
className="flex items-center gap-1 text-destructive hover:text-destructive"
title="Delete agent"
title={t('agents.deleteAgent')}
>
<Trash2 className="h-3 w-3" />
Delete
{t('app.delete')}
</Button>
</CardFooter>
</Card>
@@ -471,10 +473,10 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Previous
{t('app.previous')}
</Button>
<span className="flex items-center px-3 text-sm">
Page {currentPage} of {totalPages}
{t('app.page')} {currentPage} {t('app.of')} {totalPages}
</span>
<Button
size="sm"
@@ -482,7 +484,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Next
{t('app.next')}
</Button>
</div>
)}
@@ -495,7 +497,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
<div className="overflow-hidden">
<div className="flex items-center gap-2 mb-4">
<History className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold">Recent Executions</h2>
<h2 className="text-lg font-semibold">{t('agents.recentExecutions')}</h2>
</div>
{runsLoading ? (
<div className="flex items-center justify-center h-32">
@@ -531,7 +533,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
onImportSuccess={async () => {
setShowGitHubBrowser(false);
await loadAgents();
setToast({ message: "Agent imported successfully from GitHub", type: "success" });
setToast({ message: t('agents.importFromGitHubSuccess'), type: "success" });
}}
/>
@@ -541,11 +543,10 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
Delete Agent
{t('agents.deleteAgentTitle')}
</DialogTitle>
<DialogDescription>
Are you sure you want to delete the agent "{agentToDelete?.name}"?
This action cannot be undone and will permanently remove the agent and all its associated data.
{t('agents.deleteConfirmation', { name: agentToDelete?.name })}
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
@@ -555,7 +556,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
disabled={isDeleting}
className="w-full sm:w-auto"
>
Cancel
{t('app.cancel')}
</Button>
<Button
variant="destructive"
@@ -566,12 +567,12 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
{isDeleting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
Deleting...
{t('agents.deleting')}
</>
) : (
<>
<Trash2 className="h-4 w-4 mr-2" />
Delete Agent
{t('agents.deleteAgentButton')}
</>
)}
</Button>

View File

@@ -14,6 +14,7 @@ import { SelectComponent, type SelectOption } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { api, type CheckpointStrategy } from "@/lib/api";
import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
interface CheckpointSettingsProps {
sessionId: string;
@@ -40,6 +41,7 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
onClose,
className,
}) => {
const { t } = useTranslation();
const [autoCheckpointEnabled, setAutoCheckpointEnabled] = useState(true);
const [checkpointStrategy, setCheckpointStrategy] = useState<CheckpointStrategy>("smart");
const [totalCheckpoints, setTotalCheckpoints] = useState(0);
@@ -50,10 +52,10 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const strategyOptions: SelectOption[] = [
{ value: "manual", label: "Manual Only" },
{ value: "per_prompt", label: "After Each Prompt" },
{ value: "per_tool_use", label: "After Tool Use" },
{ value: "smart", label: "Smart (Recommended)" },
{ value: "manual", label: t('checkpoint.manualOnly') },
{ value: "per_prompt", label: t('checkpoint.afterEachPrompt') },
{ value: "per_tool_use", label: t('checkpoint.afterToolUse') },
{ value: "smart", label: t('checkpoint.smart') },
];
useEffect(() => {
@@ -71,7 +73,7 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
setTotalCheckpoints(settings.total_checkpoints);
} catch (err) {
console.error("Failed to load checkpoint settings:", err);
setError("Failed to load checkpoint settings");
setError(t('checkpoint.checkpointSettingsFailed'));
} finally {
setIsLoading(false);
}
@@ -91,11 +93,11 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
checkpointStrategy
);
setSuccessMessage("Settings saved successfully");
setSuccessMessage(t('messages.saveSuccess'));
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err) {
console.error("Failed to save checkpoint settings:", err);
setError("Failed to save checkpoint settings");
setError(t('checkpoint.saveCheckpointSettingsFailed'));
} finally {
setIsSaving(false);
}
@@ -114,14 +116,14 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
keepCount
);
setSuccessMessage(`Removed ${removed} old checkpoints`);
setSuccessMessage(t('checkpoint.removedOldCheckpoints', { count: removed }));
setTimeout(() => setSuccessMessage(null), 3000);
// Reload settings to get updated count
await loadSettings();
} catch (err) {
console.error("Failed to cleanup checkpoints:", err);
setError("Failed to cleanup checkpoints");
setError(t('checkpoint.cleanupCheckpointsFailed'));
} finally {
setIsLoading(false);
}
@@ -137,11 +139,11 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5" />
<h3 className="text-lg font-semibold">Checkpoint Settings</h3>
<h3 className="text-lg font-semibold">{t('checkpoint.checkpointSettingsTitle')}</h3>
</div>
{onClose && (
<Button variant="ghost" size="sm" onClick={onClose}>
Close
{t('app.close')}
</Button>
)}
</div>
@@ -151,9 +153,9 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-yellow-600 mt-0.5" />
<div className="text-xs">
<p className="font-medium text-yellow-600">Experimental Feature</p>
<p className="font-medium text-yellow-600">{t('checkpoint.experimentalFeature')}</p>
<p className="text-yellow-600/80">
Checkpointing may affect directory structure or cause data loss. Use with caution.
{t('checkpoint.checkpointWarning')}
</p>
</div>
</div>
@@ -186,9 +188,9 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
{/* Auto-checkpoint toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="auto-checkpoint">Automatic Checkpoints</Label>
<Label htmlFor="auto-checkpoint">{t('checkpoint.automaticCheckpoints')}</Label>
<p className="text-sm text-muted-foreground">
Automatically create checkpoints based on the selected strategy
{t('checkpoint.automaticCheckpointsDesc')}
</p>
</div>
<Switch
@@ -201,7 +203,7 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
{/* Checkpoint strategy */}
<div className="space-y-2">
<Label htmlFor="strategy">Checkpoint Strategy</Label>
<Label htmlFor="strategy">{t('checkpoint.checkpointStrategy')}</Label>
<SelectComponent
value={checkpointStrategy}
onValueChange={(value: string) => setCheckpointStrategy(value as CheckpointStrategy)}
@@ -209,10 +211,10 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
disabled={isLoading || !autoCheckpointEnabled}
/>
<p className="text-xs text-muted-foreground">
{checkpointStrategy === "manual" && "Checkpoints will only be created manually"}
{checkpointStrategy === "per_prompt" && "A checkpoint will be created after each user prompt"}
{checkpointStrategy === "per_tool_use" && "A checkpoint will be created after each tool use"}
{checkpointStrategy === "smart" && "Checkpoints will be created after destructive operations"}
{checkpointStrategy === "manual" && t('checkpoint.manualOnlyDesc')}
{checkpointStrategy === "per_prompt" && t('checkpoint.afterEachPromptDesc')}
{checkpointStrategy === "per_tool_use" && t('checkpoint.afterToolUseDesc')}
{checkpointStrategy === "smart" && t('checkpoint.smartDesc')}
</p>
</div>
@@ -225,12 +227,12 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
{isSaving ? (
<>
<Save className="h-4 w-4 mr-2 animate-spin" />
Saving...
{t('checkpoint.saving')}
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
Save Settings
{t('checkpoint.saveSettings')}
</>
)}
</Button>
@@ -239,9 +241,9 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
<div className="border-t pt-6 space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>Storage Management</Label>
<Label>{t('checkpoint.storageManagement')}</Label>
<p className="text-sm text-muted-foreground">
Total checkpoints: {totalCheckpoints}
{t('checkpoint.totalCheckpoints')}: {totalCheckpoints}
</p>
</div>
<HardDrive className="h-5 w-5 text-muted-foreground" />
@@ -249,7 +251,7 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
{/* Cleanup settings */}
<div className="space-y-2">
<Label htmlFor="keep-count">Keep Recent Checkpoints</Label>
<Label htmlFor="keep-count">{t('checkpoint.keepRecentCheckpoints')}</Label>
<div className="flex gap-2">
<Input
id="keep-count"
@@ -267,11 +269,11 @@ export const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({
disabled={isLoading || totalCheckpoints <= keepCount}
>
<Trash2 className="h-4 w-4 mr-2" />
Clean Up
{t('checkpoint.cleanUp')}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Remove old checkpoints, keeping only the most recent {keepCount}
{t('checkpoint.removeOldCheckpoints', { count: keepCount })}
</p>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ExternalLink, FileQuestion, Terminal, AlertCircle, Loader2 } from "lucide-react";
import { ClaudeVersionSelector } from "./ClaudeVersionSelector";
import { useTranslation } from "@/hooks/useTranslation";
interface ClaudeBinaryDialogProps {
open: boolean;
@@ -13,6 +14,7 @@ interface ClaudeBinaryDialogProps {
}
export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: ClaudeBinaryDialogProps) {
const { t } = useTranslation();
const [selectedInstallation, setSelectedInstallation] = useState<ClaudeInstallation | null>(null);
const [isValidating, setIsValidating] = useState(false);
const [hasInstallations, setHasInstallations] = useState(true);
@@ -39,7 +41,7 @@ export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: C
const handleSave = async () => {
if (!selectedInstallation) {
onError("Please select a Claude installation");
onError(t('pleaseSelectInstallation'));
return;
}
@@ -62,29 +64,27 @@ export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: C
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileQuestion className="w-5 h-5" />
Select Claude Code Installation
{t('selectClaudeCodeInstallation')}
</DialogTitle>
<DialogDescription className="space-y-3 mt-4">
{checkingInstallations ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">Searching for Claude installations...</span>
<span className="ml-2 text-sm text-muted-foreground">{t('searchingInstallations')}</span>
</div>
) : hasInstallations ? (
<p>
Multiple Claude Code installations were found on your system.
Please select which one you'd like to use.
{t('multipleInstallationsFound')}
</p>
) : (
<>
<p>
Claude Code was not found in any of the common installation locations.
Please install Claude Code to continue.
{t('claudeCodeNotFoundDialog')}
</p>
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
<AlertCircle className="w-4 h-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
<span className="font-medium">Searched locations:</span> PATH, /usr/local/bin,
<span className="font-medium">{t('searchedLocations')}:</span> PATH, /usr/local/bin,
/opt/homebrew/bin, ~/.nvm/versions/node/*/bin, ~/.claude/local, ~/.local/bin
</p>
</div>
@@ -94,7 +94,7 @@ export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: C
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
<Terminal className="w-4 h-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
<span className="font-medium">Tip:</span> You can install Claude Code using{" "}
<span className="font-medium">{t('validation.required')}:</span> {t('installationTip')}{" "}
<code className="px-1 py-0.5 bg-black/10 dark:bg-white/10 rounded">npm install -g @claude</code>
</p>
</div>
@@ -118,20 +118,20 @@ export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: C
className="mr-auto"
>
<ExternalLink className="w-4 h-4 mr-2" />
Installation Guide
{t('installationGuide')}
</Button>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isValidating}
>
Cancel
{t('app.cancel')}
</Button>
<Button
onClick={handleSave}
disabled={isValidating || !selectedInstallation || !hasInstallations}
>
{isValidating ? "Validating..." : hasInstallations ? "Save Selection" : "No Installations Found"}
{isValidating ? t('validating') : hasInstallations ? t('saveSelection') : t('noInstallationsFound')}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { Toast, ToastContainer } from "@/components/ui/toast";
import { api, type ClaudeMdFile } from "@/lib/api";
import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
interface ClaudeFileEditorProps {
/**
@@ -36,6 +37,7 @@ export const ClaudeFileEditor: React.FC<ClaudeFileEditorProps> = ({
onBack,
className,
}) => {
const { t } = useTranslation();
const [content, setContent] = useState<string>("");
const [originalContent, setOriginalContent] = useState<string>("");
const [loading, setLoading] = useState(true);
@@ -59,7 +61,7 @@ export const ClaudeFileEditor: React.FC<ClaudeFileEditorProps> = ({
setOriginalContent(fileContent);
} catch (err) {
console.error("Failed to load file:", err);
setError("Failed to load CLAUDE.md file");
setError(t('loadFileFailed'));
} finally {
setLoading(false);
}
@@ -72,11 +74,11 @@ export const ClaudeFileEditor: React.FC<ClaudeFileEditorProps> = ({
setToast(null);
await api.saveClaudeMdFile(file.absolute_path, content);
setOriginalContent(content);
setToast({ message: "File saved successfully", type: "success" });
setToast({ message: t('fileSavedSuccess'), type: "success" });
} catch (err) {
console.error("Failed to save file:", err);
setError("Failed to save CLAUDE.md file");
setToast({ message: "Failed to save file", type: "error" });
setError(t('saveFileFailed'));
setToast({ message: t('saveFileFailed'), type: "error" });
} finally {
setSaving(false);
}
@@ -85,7 +87,7 @@ export const ClaudeFileEditor: React.FC<ClaudeFileEditorProps> = ({
const handleBack = () => {
if (hasChanges) {
const confirmLeave = window.confirm(
"You have unsaved changes. Are you sure you want to leave?"
t('unsavedChangesConfirm')
);
if (!confirmLeave) return;
}
@@ -114,7 +116,7 @@ export const ClaudeFileEditor: React.FC<ClaudeFileEditorProps> = ({
<div className="min-w-0 flex-1">
<h2 className="text-lg font-semibold truncate">{file.relative_path}</h2>
<p className="text-xs text-muted-foreground">
Edit project-specific Claude Code system prompt
{t('editProjectSpecificPrompt')}
</p>
</div>
</div>
@@ -129,7 +131,7 @@ export const ClaudeFileEditor: React.FC<ClaudeFileEditorProps> = ({
) : (
<Save className="mr-2 h-4 w-4" />
)}
{saving ? "Saving..." : "Save"}
{saving ? t('saving') : t('app.save')}
</Button>
</motion.div>

View File

@@ -7,6 +7,7 @@ import { Label } from "@/components/ui/label";
import { api, type ClaudeInstallation } from "@/lib/api";
import { cn } from "@/lib/utils";
import { CheckCircle, HardDrive, Settings } from "lucide-react";
import { useTranslation } from "@/hooks/useTranslation";
interface ClaudeVersionSelectorProps {
/**
@@ -53,6 +54,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
onSave,
isSaving = false,
}) => {
const { t } = useTranslation();
const [installations, setInstallations] = useState<ClaudeInstallation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -132,8 +134,8 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
return (
<Card className={className}>
<CardHeader>
<CardTitle>Claude Code Installation</CardTitle>
<CardDescription>Loading available installations...</CardDescription>
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.loadingAvailableInstallations')}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-4">
@@ -148,13 +150,13 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
return (
<Card className={className}>
<CardHeader>
<CardTitle>Claude Code Installation</CardTitle>
<CardDescription>Error loading installations</CardDescription>
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.errorLoadingInstallations')}</CardDescription>
</CardHeader>
<CardContent>
<div className="text-sm text-destructive mb-4">{error}</div>
<Button onClick={loadInstallations} variant="outline" size="sm">
Retry
{t('app.retry')}
</Button>
</CardContent>
</Card>
@@ -169,16 +171,16 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="h-5 w-5" />
Claude Code Installation
{t('settings.claudeCodeInstallation')}
</CardTitle>
<CardDescription>
Choose your preferred Claude Code installation.
{t('settings.choosePreferredInstallation')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Available Installations */}
<div className="space-y-3">
<Label className="text-sm font-medium">Available Installations</Label>
<Label className="text-sm font-medium">{t('settings.availableInstallations')}</Label>
<Select value={selectedInstallation?.path || ""} onValueChange={handleInstallationChange}>
<SelectTrigger>
<SelectValue placeholder="Select Claude installation">

View File

@@ -59,12 +59,12 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
const handleSave = async () => {
if (!name.trim()) {
setError("Agent name is required");
setError(t('agents.agentNameRequired'));
return;
}
if (!systemPrompt.trim()) {
setError("System prompt is required");
setError(t('agents.systemPromptRequired'));
return;
}
@@ -181,24 +181,24 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
{/* Basic Information */}
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium mb-4">Basic Information</h3>
<h3 className="text-sm font-medium mb-4">{t('agents.basicInformation')}</h3>
</div>
{/* Name and Icon */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Agent Name</Label>
<Label htmlFor="name">{t('agents.agentName')}</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Code Assistant"
placeholder={t('placeholders.enterAgentName')}
required
/>
</div>
<div className="space-y-2">
<Label>Agent Icon</Label>
<Label>{t('agents.agentIcon')}</Label>
<div
onClick={() => setShowIconPicker(true)}
className="h-10 px-3 py-2 bg-background border border-input rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors flex items-center justify-between"
@@ -221,7 +221,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
{/* Model Selection */}
<div className="space-y-2">
<Label>Model</Label>
<Label>{t('agents.model')}</Label>
<div className="flex flex-col sm:flex-row gap-3">
<button
type="button"
@@ -245,7 +245,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
</div>
<div className="text-left">
<div className="text-sm font-semibold">Claude 4 Sonnet</div>
<div className="text-xs opacity-80">Faster, efficient for most tasks</div>
<div className="text-xs opacity-80">{t('agents.sonnetDescription')}</div>
</div>
</div>
</button>
@@ -272,7 +272,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
</div>
<div className="text-left">
<div className="text-sm font-semibold">Claude 4 Opus</div>
<div className="text-xs opacity-80">More capable, better for complex tasks</div>
<div className="text-xs opacity-80">{t('agents.opusDescription')}</div>
</div>
</div>
</button>
@@ -281,25 +281,25 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
{/* Default Task */}
<div className="space-y-2">
<Label htmlFor="default-task">Default Task (Optional)</Label>
<Label htmlFor="default-task">{t('agents.defaultTask')} ({t('agents.optional')})</Label>
<Input
id="default-task"
type="text"
placeholder="e.g., Review this code for security issues"
placeholder={t('placeholders.enterDefaultTask')}
value={defaultTask}
onChange={(e) => setDefaultTask(e.target.value)}
className="max-w-md"
/>
<p className="text-xs text-muted-foreground">
This will be used as the default task placeholder when executing the agent
{t('agents.defaultTaskDescription')}
</p>
</div>
{/* System Prompt Editor */}
<div className="space-y-2">
<Label>System Prompt</Label>
<Label>{t('agents.systemPrompt')}</Label>
<p className="text-xs text-muted-foreground mb-2">
Define the behavior and capabilities of your CC Agent
{t('agents.systemPromptDescription')}
</p>
<div className="rounded-lg border border-border overflow-hidden shadow-sm" data-color-mode="dark">
<MDEditor

View File

@@ -20,6 +20,7 @@ import { SlashCommandPicker } from "./SlashCommandPicker";
import { ImagePreview } from "./ImagePreview";
import { type FileEntry, type SlashCommand } from "@/lib/api";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { useTranslation } from "@/hooks/useTranslation";
interface FloatingPromptInputProps {
/**
@@ -72,42 +73,7 @@ type ThinkingModeConfig = {
phrase?: string; // The phrase to append
};
const THINKING_MODES: ThinkingModeConfig[] = [
{
id: "auto",
name: "Auto",
description: "Let Claude decide",
level: 0
},
{
id: "think",
name: "Think",
description: "Basic reasoning",
level: 1,
phrase: "think"
},
{
id: "think_hard",
name: "Think Hard",
description: "Deeper analysis",
level: 2,
phrase: "think hard"
},
{
id: "think_harder",
name: "Think Harder",
description: "Extensive reasoning",
level: 3,
phrase: "think harder"
},
{
id: "ultrathink",
name: "Ultrathink",
description: "Maximum computation",
level: 4,
phrase: "ultrathink"
}
];
// Thinking modes configuration will be defined inside the component to use translations
/**
* ThinkingModeIndicator component - Shows visual indicator bars for thinking level
@@ -173,6 +139,45 @@ const FloatingPromptInputInner = (
}: FloatingPromptInputProps,
ref: React.Ref<FloatingPromptInputRef>,
) => {
const { t } = useTranslation();
// Define THINKING_MODES inside component to access translations
const THINKING_MODES: ThinkingModeConfig[] = [
{
id: "auto",
name: "Auto",
description: t('messages.letClaudeDecide'),
level: 0
},
{
id: "think",
name: "Think",
description: t('messages.basicReasoning'),
level: 1,
phrase: "think"
},
{
id: "think_hard",
name: "Think Hard",
description: t('messages.deeperAnalysis'),
level: 2,
phrase: "think hard"
},
{
id: "think_harder",
name: "Think Harder",
description: t('messages.extensiveReasoning'),
level: 3,
phrase: "think harder"
},
{
id: "ultrathink",
name: "Ultrathink",
description: t('messages.maximumAnalysis'),
level: 4,
phrase: "ultrathink"
}
];
const [prompt, setPrompt] = useState("");
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
const [selectedThinkingMode, setSelectedThinkingMode] = useState<ThinkingMode>("auto");
@@ -758,7 +763,7 @@ const FloatingPromptInputInner = (
value={prompt}
onChange={handleTextChange}
onPaste={handlePaste}
placeholder="Type your prompt here..."
placeholder={t('messages.typeYourPromptHere')}
className="min-h-[200px] resize-none"
disabled={disabled}
onDragEnter={handleDrag}
@@ -1001,7 +1006,7 @@ const FloatingPromptInputInner = (
onChange={handleTextChange}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={dragActive ? "Drop images here..." : "Ask Claude anything..."}
placeholder={dragActive ? t('messages.dropImagesHere') : t('messages.askClaudeAnything')}
disabled={disabled}
className={cn(
"min-h-[44px] max-h-[120px] resize-none pr-10",
@@ -1065,7 +1070,7 @@ const FloatingPromptInputInner = (
</div>
<div className="mt-2 text-xs text-muted-foreground">
Press Enter to send, Shift+Enter for new line{projectPath?.trim() && ", @ to mention files, / for commands, drag & drop or paste images"}
{t('input.pressEnterToSend')}{projectPath?.trim() && t('input.withFileAndCommandSupport')}
</div>
</div>
</div>

View File

@@ -51,6 +51,7 @@ import {
import { cn } from '@/lib/utils';
import { HooksManager } from '@/lib/hooksManager';
import { api } from '@/lib/api';
import { useTranslation } from '@/hooks/useTranslation';
import {
HooksConfiguration,
HookEvent,
@@ -116,6 +117,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
onChange,
hideActions = false
}) => {
const { t } = useTranslation();
const [selectedEvent, setSelectedEvent] = useState<HookEvent>('PreToolUse');
const [showTemplateDialog, setShowTemplateDialog] = useState(false);
const [validationErrors, setValidationErrors] = useState<string[]>([]);
@@ -754,7 +756,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{/* Header */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Hooks Configuration</h3>
<h3 className="text-lg font-semibold">{t('hooks.hooksConfiguration')}</h3>
<div className="flex items-center gap-2">
<Badge variant={scope === 'project' ? 'secondary' : scope === 'local' ? 'outline' : 'default'}>
{scope === 'project' ? 'Project' : scope === 'local' ? 'Local' : 'User'} Scope
@@ -789,12 +791,12 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
</div>
</div>
<p className="text-sm text-muted-foreground">
Configure shell commands to execute at various points in Claude Code's lifecycle.
{scope === 'local' && ' These settings are not committed to version control.'}
{t('hooks.configureShellCommands')}
{scope === 'local' && t('hooks.localSettingsNote')}
</p>
{hasUnsavedChanges && !readOnly && (
<p className="text-sm text-amber-600">
You have unsaved changes. Click Save to persist them.
{t('hooks.unsavedChanges')}
</p>
)}
</div>

View File

@@ -8,6 +8,7 @@ import { SelectComponent } from "@/components/ui/select";
import { Card } from "@/components/ui/card";
import { api } from "@/lib/api";
import { useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
interface MCPAddServerProps {
/**
@@ -34,6 +35,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onServerAdded,
onError,
}) => {
const { t } = useTranslation();
const [transport, setTransport] = useState<"stdio" | "sse">("stdio");
const [saving, setSaving] = useState(false);
@@ -101,12 +103,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
*/
const handleAddStdioServer = async () => {
if (!stdioName.trim()) {
onError("Server name is required");
onError(t('serverNameRequired'));
return;
}
if (!stdioCommand.trim()) {
onError("Command is required");
onError(t('commandRequired'));
return;
}
@@ -152,7 +154,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onError(result.message);
}
} catch (error) {
onError("Failed to add server");
onError(t('failedToAddServer'));
console.error("Failed to add stdio server:", error);
} finally {
setSaving(false);
@@ -164,12 +166,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
*/
const handleAddSseServer = async () => {
if (!sseName.trim()) {
onError("Server name is required");
onError(t('serverNameRequired'));
return;
}
if (!sseUrl.trim()) {
onError("URL is required");
onError(t('urlRequired'));
return;
}
@@ -211,7 +213,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onError(result.message);
}
} catch (error) {
onError("Failed to add server");
onError(t('failedToAddServer'));
console.error("Failed to add SSE server:", error);
} finally {
setSaving(false);
@@ -225,7 +227,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Environment Variables</Label>
<Label className="text-sm font-medium">{t('environmentVariables')}</Label>
<Button
variant="outline"
size="sm"
@@ -233,7 +235,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="gap-2"
>
<Plus className="h-3 w-3" />
Add Variable
{t('addVariable')}
</Button>
</div>
@@ -273,9 +275,9 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
return (
<div className="p-6 space-y-6">
<div>
<h3 className="text-base font-semibold">Add MCP Server</h3>
<h3 className="text-base font-semibold">{t('addMcpServer')}</h3>
<p className="text-sm text-muted-foreground mt-1">
Configure a new Model Context Protocol server
{t('configureNewMcpServer')}
</p>
</div>
@@ -296,7 +298,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
<Card className="p-6 space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="stdio-name">Server Name</Label>
<Label htmlFor="stdio-name">{t('serverName')}</Label>
<Input
id="stdio-name"
placeholder="my-server"
@@ -304,12 +306,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onChange={(e) => setStdioName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
A unique name to identify this server
{t('uniqueNameToIdentify')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="stdio-command">Command</Label>
<Label htmlFor="stdio-command">{t('command')}</Label>
<Input
id="stdio-command"
placeholder="/path/to/server"
@@ -318,12 +320,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
The command to execute the server
{t('commandToExecuteServer')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="stdio-args">Arguments (optional)</Label>
<Label htmlFor="stdio-args">{t('argumentsOptional')}</Label>
<Input
id="stdio-args"
placeholder="arg1 arg2 arg3"
@@ -332,19 +334,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
Space-separated command arguments
{t('spaceSeparatedArgs')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="stdio-scope">Scope</Label>
<Label htmlFor="stdio-scope">{t('scope')}</Label>
<SelectComponent
value={stdioScope}
onValueChange={(value: string) => setStdioScope(value)}
options={[
{ value: "local", label: "Local (this project only)" },
{ value: "project", label: "Project (shared via .mcp.json)" },
{ value: "user", label: "User (all projects)" },
{ value: "local", label: t('localProjectOnly') },
{ value: "project", label: t('projectSharedViaMcp') },
{ value: "user", label: t('userAllProjects') },
]}
/>
</div>
@@ -361,12 +363,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Adding Server...
{t('addingServer')}
</>
) : (
<>
<Plus className="h-4 w-4" />
Add Stdio Server
{t('addStdioServer')}
</>
)}
</Button>
@@ -379,7 +381,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
<Card className="p-6 space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="sse-name">Server Name</Label>
<Label htmlFor="sse-name">{t('serverName')}</Label>
<Input
id="sse-name"
placeholder="sse-server"
@@ -387,12 +389,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onChange={(e) => setSseName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
A unique name to identify this server
{t('uniqueNameToIdentify')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="sse-url">URL</Label>
<Label htmlFor="sse-url">{t('url')}</Label>
<Input
id="sse-url"
placeholder="https://example.com/sse-endpoint"
@@ -401,19 +403,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
The SSE endpoint URL
{t('sseEndpointUrl')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="sse-scope">Scope</Label>
<Label htmlFor="sse-scope">{t('scope')}</Label>
<SelectComponent
value={sseScope}
onValueChange={(value: string) => setSseScope(value)}
options={[
{ value: "local", label: "Local (this project only)" },
{ value: "project", label: "Project (shared via .mcp.json)" },
{ value: "user", label: "User (all projects)" },
{ value: "local", label: t('localProjectOnly') },
{ value: "project", label: t('projectSharedViaMcp') },
{ value: "user", label: t('userAllProjects') },
]}
/>
</div>
@@ -430,12 +432,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Adding Server...
{t('addingServer')}
</>
) : (
<>
<Plus className="h-4 w-4" />
Add SSE Server
{t('addSseServer')}
</>
)}
</Button>
@@ -449,7 +451,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm font-medium">
<Info className="h-4 w-4 text-primary" />
<span>Example Commands</span>
<span>{t('exampleCommands')}</span>
</div>
<div className="space-y-2 text-xs text-muted-foreground">
<div className="font-mono bg-background p-2 rounded">

View File

@@ -9,6 +9,7 @@ import { api, type MCPServer } from "@/lib/api";
import { MCPServerList } from "./MCPServerList";
import { MCPAddServer } from "./MCPAddServer";
import { MCPImportExport } from "./MCPImportExport";
import { useTranslation } from "@/hooks/useTranslation";
interface MCPManagerProps {
/**
@@ -29,6 +30,7 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
onBack,
className,
}) => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState("servers");
const [servers, setServers] = useState<MCPServer[]>([]);
const [loading, setLoading] = useState(true);
@@ -55,7 +57,7 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
setServers(serverList);
} catch (err) {
console.error("MCPManager: Failed to load MCP servers:", err);
setError("Failed to load MCP servers. Make sure Claude Code is installed.");
setError(t('loadMcpServersFailed'));
} finally {
setLoading(false);
}
@@ -66,7 +68,7 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
*/
const handleServerAdded = () => {
loadServers();
setToast({ message: "MCP server added successfully!", type: "success" });
setToast({ message: t('mcpServerAdded'), type: "success" });
setActiveTab("servers");
};
@@ -75,7 +77,7 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
*/
const handleServerRemoved = (name: string) => {
setServers(prev => prev.filter(s => s.name !== name));
setToast({ message: `Server "${name}" removed successfully!`, type: "success" });
setToast({ message: t('serverRemovedSuccess', { name }), type: "success" });
};
/**
@@ -85,12 +87,12 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
loadServers();
if (failed === 0) {
setToast({
message: `Successfully imported ${imported} server${imported > 1 ? 's' : ''}!`,
message: t('importedServersSuccess', { count: imported, plural: imported > 1 ? 's' : '' }),
type: "success"
});
} else {
setToast({
message: `Imported ${imported} server${imported > 1 ? 's' : ''}, ${failed} failed`,
message: t('importedServersFailed', { imported, importedPlural: imported > 1 ? 's' : '', failed }),
type: "error"
});
}
@@ -118,10 +120,10 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
<div>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Network className="h-5 w-5 text-blue-500" />
MCP Servers
{t('mcpServers')}
</h2>
<p className="text-xs text-muted-foreground">
Manage Model Context Protocol servers
{t('manageMcpServers')}
</p>
</div>
</div>
@@ -153,15 +155,15 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
<TabsList className="grid w-full max-w-md grid-cols-3">
<TabsTrigger value="servers" className="gap-2">
<Network className="h-4 w-4 text-blue-500" />
Servers
{t('servers')}
</TabsTrigger>
<TabsTrigger value="add" className="gap-2">
<Plus className="h-4 w-4 text-green-500" />
Add Server
{t('addServer')}
</TabsTrigger>
<TabsTrigger value="import" className="gap-2">
<Download className="h-4 w-4 text-purple-500" />
Import/Export
{t('importExport')}
</TabsTrigger>
</TabsList>

View File

@@ -20,6 +20,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { api, type MCPServer } from "@/lib/api";
import { useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
interface MCPServerListProps {
/**
@@ -50,6 +51,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
onServerRemoved,
onRefresh,
}) => {
const { t } = useTranslation();
const [removingServer, setRemovingServer] = useState<string | null>(null);
const [testingServer, setTestingServer] = useState<string | null>(null);
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
@@ -184,11 +186,11 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
const getScopeDisplayName = (scope: string) => {
switch (scope) {
case "local":
return "Local (Project-specific)";
return t('localProjectSpecific');
case "project":
return "Project (Shared via .mcp.json)";
return t('projectSharedMcp');
case "user":
return "User (All projects)";
return t('userAllProjects');
default:
return scope;
}
@@ -220,7 +222,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.status?.running && (
<Badge variant="outline" className="gap-1 flex-shrink-0 border-green-500/50 text-green-600 bg-green-500/10">
<CheckCircle className="h-3 w-3" />
Running
{t('running')}
</Badge>
)}
</div>
@@ -237,7 +239,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
className="h-6 px-2 text-xs hover:bg-primary/10"
>
<ChevronDown className="h-3 w-3 mr-1" />
Show full
{t('showFull')}
</Button>
</div>
)}
@@ -299,7 +301,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.command && (
<div className="space-y-1">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">Command</p>
<p className="text-xs font-medium text-muted-foreground">{t('command')}</p>
<div className="flex items-center gap-1">
<Button
variant="ghost"
@@ -308,7 +310,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
className="h-6 px-2 text-xs hover:bg-primary/10"
>
<Copy className="h-3 w-3 mr-1" />
{isCopied ? "Copied!" : "Copy"}
{isCopied ? t('copied') : t('copy')}
</Button>
<Button
variant="ghost"
@@ -317,7 +319,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
className="h-6 px-2 text-xs hover:bg-primary/10"
>
<ChevronUp className="h-3 w-3 mr-1" />
Hide
{t('hide')}
</Button>
</div>
</div>
@@ -329,7 +331,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.args && server.args.length > 0 && (
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Arguments</p>
<p className="text-xs font-medium text-muted-foreground">{t('arguments')}</p>
<div className="text-xs font-mono bg-muted/50 p-2 rounded space-y-1">
{server.args.map((arg, idx) => (
<div key={idx} className="break-all">
@@ -343,7 +345,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.transport === "sse" && server.url && (
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">URL</p>
<p className="text-xs font-medium text-muted-foreground">{t('url')}</p>
<p className="text-xs font-mono bg-muted/50 p-2 rounded break-all">
{server.url}
</p>
@@ -352,7 +354,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{Object.keys(server.env).length > 0 && (
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Environment Variables</p>
<p className="text-xs font-medium text-muted-foreground">{t('environmentVariables')}</p>
<div className="text-xs font-mono bg-muted/50 p-2 rounded space-y-1">
{Object.entries(server.env).map(([key, value]) => (
<div key={key} className="break-all">
@@ -384,9 +386,9 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-base font-semibold">Configured Servers</h3>
<h3 className="text-base font-semibold">{t('configuredServers')}</h3>
<p className="text-sm text-muted-foreground">
{servers.length} server{servers.length !== 1 ? "s" : ""} configured
{servers.length} {servers.length !== 1 ? t('servers') : 'server'} {t('serversConfigured')}
</p>
</div>
<Button
@@ -396,7 +398,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
className="gap-2 hover:bg-primary/10 hover:text-primary hover:border-primary/50"
>
<RefreshCw className="h-4 w-4" />
Refresh
{t('app.refresh')}
</Button>
</div>
@@ -406,9 +408,9 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
<div className="p-4 bg-primary/10 rounded-full mb-4">
<Network className="h-12 w-12 text-primary" />
</div>
<p className="text-muted-foreground mb-2 font-medium">No MCP servers configured</p>
<p className="text-muted-foreground mb-2 font-medium">{t('noMcpServersConfigured')}</p>
<p className="text-sm text-muted-foreground">
Add a server to get started with Model Context Protocol
{t('addServerToGetStarted')}
</p>
</div>
) : (

View File

@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { Toast, ToastContainer } from "@/components/ui/toast";
import { api } from "@/lib/api";
import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
interface MarkdownEditorProps {
/**
@@ -28,6 +29,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
onBack,
className,
}) => {
const { t } = useTranslation();
const [content, setContent] = useState<string>("");
const [originalContent, setOriginalContent] = useState<string>("");
const [loading, setLoading] = useState(true);
@@ -51,7 +53,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
setOriginalContent(prompt);
} catch (err) {
console.error("Failed to load system prompt:", err);
setError("Failed to load CLAUDE.md file");
setError(t('loadClaudemdFailed'));
} finally {
setLoading(false);
}
@@ -64,11 +66,11 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
setToast(null);
await api.saveSystemPrompt(content);
setOriginalContent(content);
setToast({ message: "CLAUDE.md saved successfully", type: "success" });
setToast({ message: t('claudemdSavedSuccess'), type: "success" });
} catch (err) {
console.error("Failed to save system prompt:", err);
setError("Failed to save CLAUDE.md file");
setToast({ message: "Failed to save CLAUDE.md", type: "error" });
setError(t('saveClaudemdFailed'));
setToast({ message: t('saveClaudemdFailed'), type: "error" });
} finally {
setSaving(false);
}
@@ -77,7 +79,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
const handleBack = () => {
if (hasChanges) {
const confirmLeave = window.confirm(
"You have unsaved changes. Are you sure you want to leave?"
t('unsavedChangesConfirm')
);
if (!confirmLeave) return;
}
@@ -106,7 +108,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
<div>
<h2 className="text-lg font-semibold">CLAUDE.md</h2>
<p className="text-xs text-muted-foreground">
Edit your Claude Code system prompt
{t('editSystemPrompt')}
</p>
</div>
</div>
@@ -121,7 +123,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
) : (
<Save className="mr-2 h-4 w-4" />
)}
{saving ? "Saving..." : "Save"}
{saving ? t('app.saving') : t('app.save')}
</Button>
</motion.div>

View File

@@ -17,6 +17,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTranslation } from "@/hooks/useTranslation";
import type { Project } from "@/lib/api";
import { cn } from "@/lib/utils";
import { formatTimeAgo } from "@/lib/date-utils";
@@ -70,6 +71,7 @@ export const ProjectList: React.FC<ProjectListProps> = ({
onProjectSettings,
className,
}) => {
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
// Calculate pagination
@@ -150,7 +152,7 @@ export const ProjectList: React.FC<ProjectListProps> = ({
}}
>
<Settings className="h-4 w-4 mr-2" />
Hooks
{t('settings.hooks')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { useTranslation } from '@/hooks/useTranslation';
export interface ProxySettings {
http_proxy: string | null;
@@ -18,6 +19,7 @@ interface ProxySettingsProps {
}
export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
const { t } = useTranslation();
const [settings, setSettings] = useState<ProxySettings>({
http_proxy: null,
https_proxy: null,
@@ -43,13 +45,13 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
await invoke('save_proxy_settings', { settings });
setOriginalSettings(settings);
setToast({
message: 'Proxy settings saved and applied successfully.',
message: t('settings.proxySettingsSaved'),
type: 'success',
});
} catch (error) {
console.error('Failed to save proxy settings:', error);
setToast({
message: 'Failed to save proxy settings',
message: t('settings.saveProxySettingsFailed'),
type: 'error',
});
throw error; // Re-throw to let parent handle the error
@@ -72,7 +74,7 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
} catch (error) {
console.error('Failed to load proxy settings:', error);
setToast({
message: 'Failed to load proxy settings',
message: t('settings.loadProxySettingsFailed'),
type: 'error',
});
}
@@ -89,18 +91,18 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">Proxy Settings</h3>
<h3 className="text-lg font-medium">{t('settings.proxySettingsTitle')}</h3>
<p className="text-sm text-muted-foreground">
Configure proxy settings for Claude API requests
{t('settings.proxySettingsDesc')}
</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="proxy-enabled">Enable Proxy</Label>
<Label htmlFor="proxy-enabled">{t('settings.enableProxy')}</Label>
<p className="text-sm text-muted-foreground">
Use proxy for all Claude API requests
{t('settings.enableProxyDesc')}
</p>
</div>
<Switch
@@ -112,7 +114,7 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
<div className="space-y-4" style={{ opacity: settings.enabled ? 1 : 0.5 }}>
<div className="space-y-2">
<Label htmlFor="http-proxy">HTTP Proxy</Label>
<Label htmlFor="http-proxy">{t('settings.httpProxy')}</Label>
<Input
id="http-proxy"
placeholder="http://proxy.example.com:8080"
@@ -123,7 +125,7 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
</div>
<div className="space-y-2">
<Label htmlFor="https-proxy">HTTPS Proxy</Label>
<Label htmlFor="https-proxy">{t('settings.httpsProxy')}</Label>
<Input
id="https-proxy"
placeholder="http://proxy.example.com:8080"
@@ -134,7 +136,7 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
</div>
<div className="space-y-2">
<Label htmlFor="no-proxy">No Proxy</Label>
<Label htmlFor="no-proxy">{t('settings.noProxy')}</Label>
<Input
id="no-proxy"
placeholder="localhost,127.0.0.1,.example.com"
@@ -143,12 +145,12 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
disabled={!settings.enabled}
/>
<p className="text-xs text-muted-foreground">
Comma-separated list of hosts that should bypass the proxy
{t('settings.noProxyDesc')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="all-proxy">All Proxy (Optional)</Label>
<Label htmlFor="all-proxy">All Proxy ({t('agents.optional')})</Label>
<Input
id="all-proxy"
placeholder="socks5://proxy.example.com:1080"
@@ -157,7 +159,7 @@ export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
disabled={!settings.enabled}
/>
<p className="text-xs text-muted-foreground">
Proxy URL to use for all protocols if protocol-specific proxies are not set
{t('settings.allProxyDesc')}
</p>
</div>
</div>

View File

@@ -31,7 +31,7 @@ import { HooksEditor } from "./HooksEditor";
import { SlashCommandsManager } from "./SlashCommandsManager";
import { ProxySettings } from "./ProxySettings";
import { AnalyticsConsent } from "./AnalyticsConsent";
import { useTheme, useTrackEvent } from "@/hooks";
import { useTheme, useTrackEvent, useTranslation } from "@/hooks";
import { analytics } from "@/lib/analytics";
interface SettingsProps {
@@ -64,6 +64,7 @@ export const Settings: React.FC<SettingsProps> = ({
onBack,
className,
}) => {
const { t } = useTranslation();
const [settings, setSettings] = useState<ClaudeSettings | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -178,7 +179,7 @@ export const Settings: React.FC<SettingsProps> = ({
}
} catch (err) {
console.error("Failed to load settings:", err);
setError("Failed to load settings. Please ensure ~/.claude directory exists.");
setError(t('settings.messages.loadFailed'));
setSettings({});
} finally {
setLoading(false);
@@ -232,11 +233,11 @@ export const Settings: React.FC<SettingsProps> = ({
setProxySettingsChanged(false);
}
setToast({ message: "Settings saved successfully!", type: "success" });
setToast({ message: t('settings.saveButton.settingsSavedSuccess'), type: "success" });
} catch (err) {
console.error("Failed to save settings:", err);
setError("Failed to save settings.");
setToast({ message: "Failed to save settings", type: "error" });
setError(t('settings.messages.saveFailed'));
setToast({ message: t('settings.saveButton.settingsSaveFailed'), type: "error" });
} finally {
setSaving(false);
}
@@ -347,9 +348,9 @@ export const Settings: React.FC<SettingsProps> = ({
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h2 className="text-lg font-semibold">Settings</h2>
<h2 className="text-lg font-semibold">{t('settings.title')}</h2>
<p className="text-xs text-muted-foreground">
Configure Claude Code preferences
{t('settings.configurePreferences')}
</p>
</div>
</div>
@@ -363,12 +364,12 @@ export const Settings: React.FC<SettingsProps> = ({
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Saving...
{t('settings.saveButton.saving')}
</>
) : (
<>
<Save className="h-4 w-4" />
Save Settings
{t('settings.saveButton.saveSettings')}
</>
)}
</Button>
@@ -398,55 +399,55 @@ export const Settings: React.FC<SettingsProps> = ({
<div className="flex-1 overflow-y-auto p-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid grid-cols-9 w-full">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="permissions">Permissions</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
<TabsTrigger value="hooks">Hooks</TabsTrigger>
<TabsTrigger value="commands">Commands</TabsTrigger>
<TabsTrigger value="storage">Storage</TabsTrigger>
<TabsTrigger value="proxy">Proxy</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="general">{t('settings.general')}</TabsTrigger>
<TabsTrigger value="permissions">{t('settings.permissionsTab')}</TabsTrigger>
<TabsTrigger value="environment">{t('settings.environmentTab')}</TabsTrigger>
<TabsTrigger value="advanced">{t('settings.advancedTab')}</TabsTrigger>
<TabsTrigger value="hooks">{t('settings.hooksTab')}</TabsTrigger>
<TabsTrigger value="commands">{t('settings.commands')}</TabsTrigger>
<TabsTrigger value="storage">{t('settings.storage')}</TabsTrigger>
<TabsTrigger value="proxy">{t('settings.proxy')}</TabsTrigger>
<TabsTrigger value="analytics">{t('settings.analyticsTab')}</TabsTrigger>
</TabsList>
{/* General Settings */}
<TabsContent value="general" className="space-y-6">
<Card className="p-6 space-y-6">
<div>
<h3 className="text-base font-semibold mb-4">General Settings</h3>
<h3 className="text-base font-semibold mb-4">{t('settings.generalSettings')}</h3>
<div className="space-y-4">
{/* Theme Selector */}
<div className="space-y-2">
<Label htmlFor="theme">Theme</Label>
<Label htmlFor="theme">{t('settings.theme')}</Label>
<Select
value={theme}
onValueChange={(value) => setTheme(value as any)}
>
<SelectTrigger id="theme" className="w-full">
<SelectValue placeholder="Select a theme" />
<SelectValue placeholder={t('settings.themeSelector.selectATheme')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="gray">Gray</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="custom">Custom</SelectItem>
<SelectItem value="dark">{t('settings.themeSelector.dark')}</SelectItem>
<SelectItem value="gray">{t('settings.themeSelector.gray')}</SelectItem>
<SelectItem value="light">{t('settings.themeSelector.light')}</SelectItem>
<SelectItem value="custom">{t('settings.themeSelector.custom')}</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Choose your preferred color theme for the interface
{t('settings.themeSelector.choosePreferredTheme')}
</p>
</div>
{/* Custom Color Editor */}
{theme === 'custom' && (
<div className="space-y-4 p-4 border rounded-lg bg-muted/20">
<h4 className="text-sm font-medium">Custom Theme Colors</h4>
<h4 className="text-sm font-medium">{t('settings.customTheme.title')}</h4>
<div className="grid grid-cols-2 gap-4">
{/* Background Color */}
<div className="space-y-2">
<Label htmlFor="color-background" className="text-xs">Background</Label>
<Label htmlFor="color-background" className="text-xs">{t('settings.customTheme.background')}</Label>
<div className="flex gap-2">
<Input
id="color-background"
@@ -465,7 +466,7 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Foreground Color */}
<div className="space-y-2">
<Label htmlFor="color-foreground" className="text-xs">Foreground</Label>
<Label htmlFor="color-foreground" className="text-xs">{t('settings.customTheme.foreground')}</Label>
<div className="flex gap-2">
<Input
id="color-foreground"
@@ -484,7 +485,7 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Primary Color */}
<div className="space-y-2">
<Label htmlFor="color-primary" className="text-xs">Primary</Label>
<Label htmlFor="color-primary" className="text-xs">{t('settings.customTheme.primary')}</Label>
<div className="flex gap-2">
<Input
id="color-primary"
@@ -503,7 +504,7 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Card Color */}
<div className="space-y-2">
<Label htmlFor="color-card" className="text-xs">Card</Label>
<Label htmlFor="color-card" className="text-xs">{t('settings.customTheme.card')}</Label>
<div className="flex gap-2">
<Input
id="color-card"
@@ -522,7 +523,7 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Accent Color */}
<div className="space-y-2">
<Label htmlFor="color-accent" className="text-xs">Accent</Label>
<Label htmlFor="color-accent" className="text-xs">{t('settings.customTheme.accent')}</Label>
<div className="flex gap-2">
<Input
id="color-accent"
@@ -541,7 +542,7 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Destructive Color */}
<div className="space-y-2">
<Label htmlFor="color-destructive" className="text-xs">Destructive</Label>
<Label htmlFor="color-destructive" className="text-xs">{t('settings.customTheme.destructive')}</Label>
<div className="flex gap-2">
<Input
id="color-destructive"
@@ -560,7 +561,7 @@ export const Settings: React.FC<SettingsProps> = ({
</div>
<p className="text-xs text-muted-foreground">
Use CSS color values (hex, rgb, oklch, etc.). Changes apply immediately.
{t('settings.customTheme.colorValuesDesc')}
</p>
</div>
)}
@@ -568,9 +569,9 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Include Co-authored By */}
<div className="flex items-center justify-between">
<div className="space-y-0.5 flex-1">
<Label htmlFor="coauthored">Include "Co-authored by Claude"</Label>
<Label htmlFor="coauthored">{t('settings.generalOptions.includeCoAuthor')}</Label>
<p className="text-xs text-muted-foreground">
Add Claude attribution to git commits and pull requests
{t('settings.generalOptions.includeCoAuthorDesc')}
</p>
</div>
<Switch
@@ -583,9 +584,9 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Verbose Output */}
<div className="flex items-center justify-between">
<div className="space-y-0.5 flex-1">
<Label htmlFor="verbose">Verbose Output</Label>
<Label htmlFor="verbose">{t('settings.generalOptions.verboseOutput')}</Label>
<p className="text-xs text-muted-foreground">
Show full bash and command outputs
{t('settings.generalOptions.verboseOutputDesc')}
</p>
</div>
<Switch
@@ -597,7 +598,7 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Cleanup Period */}
<div className="space-y-2">
<Label htmlFor="cleanup">Chat Transcript Retention (days)</Label>
<Label htmlFor="cleanup">{t('settings.generalOptions.chatRetention')}</Label>
<Input
id="cleanup"
type="number"
@@ -610,16 +611,16 @@ export const Settings: React.FC<SettingsProps> = ({
}}
/>
<p className="text-xs text-muted-foreground">
How long to retain chat transcripts locally (default: 30 days)
{t('settings.generalOptions.chatRetentionDesc')}
</p>
</div>
{/* Claude Binary Path Selector */}
<div className="space-y-4">
<div>
<Label className="text-sm font-medium mb-2 block">Claude Code Installation</Label>
<Label className="text-sm font-medium mb-2 block">{t('settings.generalOptions.claudeCodeInstallation')}</Label>
<p className="text-xs text-muted-foreground mb-4">
Select which Claude Code installation to use.
{t('settings.generalOptions.claudeCodeInstallationDesc')}
</p>
</div>
<ClaudeVersionSelector
@@ -628,7 +629,7 @@ export const Settings: React.FC<SettingsProps> = ({
/>
{binaryPathChanged && (
<p className="text-xs text-amber-600 dark:text-amber-400">
Claude binary path has been changed. Remember to save your settings.
{t('settings.generalOptions.binaryPathChanged')}
</p>
)}
</div>
@@ -642,16 +643,16 @@ export const Settings: React.FC<SettingsProps> = ({
<Card className="p-6">
<div className="space-y-6">
<div>
<h3 className="text-base font-semibold mb-2">Permission Rules</h3>
<h3 className="text-base font-semibold mb-2">{t('settings.permissions.permissionRules')}</h3>
<p className="text-sm text-muted-foreground mb-4">
Control which tools Claude Code can use without manual approval
{t('settings.permissions.permissionRulesDesc')}
</p>
</div>
{/* Allow Rules */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-green-500">Allow Rules</Label>
<Label className="text-sm font-medium text-green-500">{t('settings.permissions.allowRules')}</Label>
<Button
variant="outline"
size="sm"
@@ -659,13 +660,13 @@ export const Settings: React.FC<SettingsProps> = ({
className="gap-2 hover:border-green-500/50 hover:text-green-500"
>
<Plus className="h-3 w-3" />
Add Rule
{t('settings.permissions.addRule')}
</Button>
</div>
<div className="space-y-2">
{allowRules.length === 0 ? (
<p className="text-xs text-muted-foreground py-2">
No allow rules configured. Claude will ask for approval for all tools.
{t('settings.permissions.noAllowRules')}
</p>
) : (
allowRules.map((rule) => (
@@ -676,7 +677,7 @@ export const Settings: React.FC<SettingsProps> = ({
className="flex items-center gap-2"
>
<Input
placeholder="e.g., Bash(npm run test:*)"
placeholder={t('settings.placeholders.allowRuleExample')}
value={rule.value}
onChange={(e) => updatePermissionRule("allow", rule.id, e.target.value)}
className="flex-1"
@@ -698,7 +699,7 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Deny Rules */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-red-500">Deny Rules</Label>
<Label className="text-sm font-medium text-red-500">{t('settings.permissions.denyRules')}</Label>
<Button
variant="outline"
size="sm"
@@ -706,13 +707,13 @@ export const Settings: React.FC<SettingsProps> = ({
className="gap-2 hover:border-red-500/50 hover:text-red-500"
>
<Plus className="h-3 w-3" />
Add Rule
{t('settings.permissions.addRule')}
</Button>
</div>
<div className="space-y-2">
{denyRules.length === 0 ? (
<p className="text-xs text-muted-foreground py-2">
No deny rules configured.
{t('settings.permissions.noDenyRules')}
</p>
) : (
denyRules.map((rule) => (
@@ -723,7 +724,7 @@ export const Settings: React.FC<SettingsProps> = ({
className="flex items-center gap-2"
>
<Input
placeholder="e.g., Bash(curl:*)"
placeholder={t('settings.placeholders.denyRuleExample')}
value={rule.value}
onChange={(e) => updatePermissionRule("deny", rule.id, e.target.value)}
className="flex-1"
@@ -744,14 +745,14 @@ export const Settings: React.FC<SettingsProps> = ({
<div className="pt-2 space-y-2">
<p className="text-xs text-muted-foreground">
<strong>Examples:</strong>
<strong>{t('settings.permissions.examples')}</strong>
</p>
<ul className="text-xs text-muted-foreground space-y-1 ml-4">
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Bash</code> - Allow all bash commands</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Bash(npm run build)</code> - Allow exact command</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Bash(npm run test:*)</code> - Allow commands with prefix</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Read(~/.zshrc)</code> - Allow reading specific file</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Edit(docs/**)</code> - Allow editing files in docs directory</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Bash</code> - {t('settings.permissions.exampleBash')}</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Bash(npm run build)</code> - {t('settings.permissions.exampleExactCommand')}</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Bash(npm run test:*)</code> - {t('settings.permissions.examplePrefix')}</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Read(~/.zshrc)</code> - {t('settings.permissions.exampleReadFile')}</li>
<li> <code className="px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Edit(docs/**)</code> - {t('settings.permissions.exampleEditDir')}</li>
</ul>
</div>
</div>
@@ -764,9 +765,9 @@ export const Settings: React.FC<SettingsProps> = ({
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-base font-semibold">Environment Variables</h3>
<h3 className="text-base font-semibold">{t('settings.environment.environmentVariables')}</h3>
<p className="text-sm text-muted-foreground mt-1">
Environment variables applied to every Claude Code session
{t('settings.environment.environmentVariablesDesc')}
</p>
</div>
<Button
@@ -776,14 +777,14 @@ export const Settings: React.FC<SettingsProps> = ({
className="gap-2"
>
<Plus className="h-3 w-3" />
Add Variable
{t('settings.environment.addVariable')}
</Button>
</div>
<div className="space-y-3">
{envVars.length === 0 ? (
<p className="text-xs text-muted-foreground py-2">
No environment variables configured.
{t('settings.environment.noEnvironmentVariables')}
</p>
) : (
envVars.map((envVar) => (
@@ -794,14 +795,14 @@ export const Settings: React.FC<SettingsProps> = ({
className="flex items-center gap-2"
>
<Input
placeholder="KEY"
placeholder={t('settings.placeholders.envVarKey')}
value={envVar.key}
onChange={(e) => updateEnvVar(envVar.id, "key", e.target.value)}
className="flex-1 font-mono text-sm"
/>
<span className="text-muted-foreground">=</span>
<Input
placeholder="value"
placeholder={t('settings.placeholders.envVarValue')}
value={envVar.value}
onChange={(e) => updateEnvVar(envVar.id, "value", e.target.value)}
className="flex-1 font-mono text-sm"
@@ -821,12 +822,12 @@ export const Settings: React.FC<SettingsProps> = ({
<div className="pt-2 space-y-2">
<p className="text-xs text-muted-foreground">
<strong>Common variables:</strong>
<strong>{t('settings.environment.commonVariables')}</strong>
</p>
<ul className="text-xs text-muted-foreground space-y-1 ml-4">
<li> <code className="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400">CLAUDE_CODE_ENABLE_TELEMETRY</code> - Enable/disable telemetry (0 or 1)</li>
<li> <code className="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400">ANTHROPIC_MODEL</code> - Custom model name</li>
<li> <code className="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400">DISABLE_COST_WARNINGS</code> - Disable cost warnings (1)</li>
<li> <code className="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400">CLAUDE_CODE_ENABLE_TELEMETRY</code> - {t('settings.environment.telemetryDesc')}</li>
<li> <code className="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400">ANTHROPIC_MODEL</code> - {t('settings.environment.modelDesc')}</li>
<li> <code className="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400">DISABLE_COST_WARNINGS</code> - {t('settings.environment.costWarningsDesc')}</li>
</ul>
</div>
</div>
@@ -837,34 +838,34 @@ export const Settings: React.FC<SettingsProps> = ({
<Card className="p-6">
<div className="space-y-6">
<div>
<h3 className="text-base font-semibold mb-4">Advanced Settings</h3>
<h3 className="text-base font-semibold mb-4">{t('settings.advanced.advancedSettings')}</h3>
<p className="text-sm text-muted-foreground mb-6">
Additional configuration options for advanced users
{t('settings.advanced.advancedSettingsDesc')}
</p>
</div>
{/* API Key Helper */}
<div className="space-y-2">
<Label htmlFor="apiKeyHelper">API Key Helper Script</Label>
<Label htmlFor="apiKeyHelper">{t('settings.advanced.apiKeyHelper')}</Label>
<Input
id="apiKeyHelper"
placeholder="/path/to/generate_api_key.sh"
placeholder={t('settings.placeholders.apiKeyHelperPath')}
value={settings?.apiKeyHelper || ""}
onChange={(e) => updateSetting("apiKeyHelper", e.target.value || undefined)}
/>
<p className="text-xs text-muted-foreground">
Custom script to generate auth values for API requests
{t('settings.advanced.apiKeyHelperDesc')}
</p>
</div>
{/* Raw JSON Editor */}
<div className="space-y-2">
<Label>Raw Settings (JSON)</Label>
<Label>{t('settings.advanced.rawSettings')}</Label>
<div className="p-3 rounded-md bg-muted font-mono text-xs overflow-x-auto whitespace-pre-wrap">
<pre>{JSON.stringify(settings, null, 2)}</pre>
</div>
<p className="text-xs text-muted-foreground">
This shows the raw JSON that will be saved to ~/.claude/settings.json
{t('settings.advanced.rawSettingsDesc')}
</p>
</div>
</div>
@@ -876,10 +877,9 @@ export const Settings: React.FC<SettingsProps> = ({
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-base font-semibold mb-2">User Hooks</h3>
<h3 className="text-base font-semibold mb-2">{t('settings.hooks.userHooks')}</h3>
<p className="text-sm text-muted-foreground mb-4">
Configure hooks that apply to all Claude Code sessions for your user account.
These are stored in <code className="mx-1 px-2 py-1 bg-muted rounded text-xs">~/.claude/settings.json</code>
{t('settings.hooks.userHooksDesc')}
</p>
</div>
@@ -928,16 +928,16 @@ export const Settings: React.FC<SettingsProps> = ({
<div>
<div className="flex items-center gap-3 mb-4">
<BarChart3 className="h-5 w-5 text-purple-600 dark:text-purple-400" />
<h3 className="text-base font-semibold">Analytics Settings</h3>
<h3 className="text-base font-semibold">{t('settings.analytics.analyticsSettings')}</h3>
</div>
<div className="space-y-6">
{/* Analytics Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="analytics-enabled" className="text-base">Enable Analytics</Label>
<Label htmlFor="analytics-enabled" className="text-base">{t('settings.analytics.enableAnalytics')}</Label>
<p className="text-sm text-muted-foreground">
Help improve Claudia by sharing anonymous usage data
{t('settings.analytics.enableAnalyticsDesc')}
</p>
</div>
<Switch
@@ -950,12 +950,12 @@ export const Settings: React.FC<SettingsProps> = ({
await analytics.enable();
setAnalyticsEnabled(true);
trackEvent.settingsChanged('analytics_enabled', true);
setToast({ message: "Analytics enabled", type: "success" });
setToast({ message: t('settings.analytics.analyticsEnabled'), type: "success" });
} else {
await analytics.disable();
setAnalyticsEnabled(false);
trackEvent.settingsChanged('analytics_enabled', false);
setToast({ message: "Analytics disabled", type: "success" });
setToast({ message: t('settings.analytics.analyticsDisabled'), type: "success" });
}
}}
/>
@@ -966,12 +966,12 @@ export const Settings: React.FC<SettingsProps> = ({
<div className="flex gap-3">
<Shield className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="space-y-2">
<p className="font-medium text-blue-900 dark:text-blue-100">Your privacy is protected</p>
<p className="font-medium text-blue-900 dark:text-blue-100">{t('settings.analytics.privacyProtected')}</p>
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
<li> No personal information is collected</li>
<li> No file contents, paths, or project names</li>
<li> All data is anonymous with random IDs</li>
<li> You can disable analytics at any time</li>
<li> {t('settings.analytics.noPersonalInfo')}</li>
<li> {t('settings.analytics.noFileContents')}</li>
<li> {t('settings.analytics.anonymousData')}</li>
<li> {t('settings.analytics.canDisable')}</li>
</ul>
</div>
</div>
@@ -981,12 +981,12 @@ export const Settings: React.FC<SettingsProps> = ({
{analyticsEnabled && (
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium mb-2">What we collect:</h4>
<h4 className="text-sm font-medium mb-2">{t('settings.analytics.whatWeCollect')}</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Feature usage patterns</li>
<li> Performance metrics</li>
<li> Error reports (without sensitive data)</li>
<li> Session frequency and duration</li>
<li> {t('settings.analytics.featureUsage')}</li>
<li> {t('settings.analytics.performanceMetrics')}</li>
<li> {t('settings.analytics.errorReports')}</li>
<li> {t('settings.analytics.sessionFrequency')}</li>
</ul>
</div>
@@ -999,11 +999,11 @@ export const Settings: React.FC<SettingsProps> = ({
await analytics.deleteAllData();
setAnalyticsEnabled(false);
setAnalyticsConsented(false);
setToast({ message: "All analytics data deleted", type: "success" });
setToast({ message: t('settings.analytics.allDataDeleted'), type: "success" });
}}
>
<Trash className="mr-2 h-4 w-4" />
Delete All Analytics Data
{t('settings.analytics.deleteAllData')}
</Button>
</div>
</div>

View File

@@ -30,6 +30,7 @@ import { api, type SlashCommand } from "@/lib/api";
import { cn } from "@/lib/utils";
import { COMMON_TOOL_MATCHERS } from "@/types/hooks";
import { useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
interface SlashCommandsManagerProps {
projectPath?: string;
@@ -92,6 +93,7 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
className,
scopeFilter = 'all',
}) => {
const { t } = useTranslation();
const [commands, setCommands] = useState<SlashCommand[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -305,17 +307,17 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">
{scopeFilter === 'project' ? 'Project Slash Commands' : 'Slash Commands'}
{scopeFilter === 'project' ? t('slashCommands.projectSlashCommands') : t('slashCommands.slashCommands')}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{scopeFilter === 'project'
? 'Create custom commands for this project'
: 'Create custom commands to streamline your workflow'}
? t('slashCommands.createCustomCommandsProject')
: t('slashCommands.createCustomCommandsWorkflow')}
</p>
</div>
<Button onClick={handleCreateNew} size="sm" className="gap-2">
<Plus className="h-4 w-4" />
New Command
{t('slashCommands.newCommand')}
</Button>
</div>
@@ -325,7 +327,7 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search commands..."
placeholder={t('placeholders.searchCommands')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
@@ -338,9 +340,9 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Commands</SelectItem>
<SelectItem value="project">Project</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="all">{t('slashCommands.allCommands')}</SelectItem>
<SelectItem value="project">{t('slashCommands.project')}</SelectItem>
<SelectItem value="user">{t('slashCommands.user')}</SelectItem>
</SelectContent>
</Select>
)}
@@ -365,16 +367,16 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
<Command className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground">
{searchQuery
? "No commands found"
? t('slashCommands.noCommandsFound')
: scopeFilter === 'project'
? "No project commands created yet"
: "No commands created yet"}
? t('slashCommands.noProjectCommandsYet')
: t('slashCommands.noCommandsYet')}
</p>
{!searchQuery && (
<Button onClick={handleCreateNew} variant="outline" size="sm" className="mt-4">
{scopeFilter === 'project'
? "Create your first project command"
: "Create your first command"}
? t('slashCommands.createFirstProjectCommand')
: t('slashCommands.createFirstCommand')}
</Button>
)}
</div>

View File

@@ -9,6 +9,7 @@ import { ProjectList } from '@/components/ProjectList';
import { SessionList } from '@/components/SessionList';
import { RunningClaudeSessions } from '@/components/RunningClaudeSessions';
import { Button } from '@/components/ui/button';
import { useTranslation } from '@/hooks/useTranslation';
// Lazy load heavy components
const ClaudeCodeSession = lazy(() => import('@/components/ClaudeCodeSession').then(m => ({ default: m.ClaudeCodeSession })));
@@ -29,6 +30,7 @@ interface TabPanelProps {
}
const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
const { t } = useTranslation();
const { updateTab, createChatTab } = useTabState();
const [projects, setProjects] = React.useState<Project[]>([]);
const [selectedProject, setSelectedProject] = React.useState<Project | null>(null);
@@ -54,7 +56,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
setProjects(projectList);
} catch (err) {
console.error("Failed to load projects:", err);
setError("Failed to load projects. Please ensure ~/.claude directory exists.");
setError(t('failedToLoadProjects'));
} finally {
setLoading(false);
}
@@ -69,7 +71,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
setSelectedProject(project);
} catch (err) {
console.error("Failed to load sessions:", err);
setError("Failed to load sessions for this project.");
setError(t('failedToLoadSessions'));
} finally {
setLoading(false);
}
@@ -96,9 +98,9 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
<div className="container mx-auto p-6">
{/* Header */}
<div className="mb-6">
<h1 className="text-3xl font-bold tracking-tight">CC Projects</h1>
<h1 className="text-3xl font-bold tracking-tight">{t('ccProjects')}</h1>
<p className="mt-1 text-sm text-muted-foreground">
Browse your Claude Code sessions
{t('browseClaudeCodeSessions')}
</p>
</div>
@@ -174,7 +176,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
className="w-full max-w-md"
>
<Plus className="mr-2 h-4 w-4" />
New Claude Code session
{t('newClaudeCodeSession')}
</Button>
</motion.div>
@@ -196,7 +198,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">
No projects found in ~/.claude/projects
{t('noProjectsFound')}
</p>
</div>
)}
@@ -217,7 +219,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
// Go back to projects view in the same tab
updateTab(tab.id, {
type: 'projects',
title: 'CC Projects',
title: t('ccProjects'),
});
}}
/>
@@ -225,7 +227,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
case 'agent':
if (!tab.agentRunId) {
return <div className="p-4">No agent run ID specified</div>;
return <div className="p-4">{t('messages.noAgentRunIdSpecified')}</div>;
}
return (
<AgentRunOutputViewer
@@ -249,15 +251,15 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
case 'claude-file':
if (!tab.claudeFileId) {
return <div className="p-4">No Claude file ID specified</div>;
return <div className="p-4">{t('messages.noClaudeFileIdSpecified')}</div>;
}
// Note: We need to get the actual file object for ClaudeFileEditor
// For now, returning a placeholder
return <div className="p-4">Claude file editor not yet implemented in tabs</div>;
return <div className="p-4">{t('messages.claudeFileEditorNotImplemented')}</div>;
case 'agent-execution':
if (!tab.agentData) {
return <div className="p-4">No agent data specified</div>;
return <div className="p-4">{t('messages.noAgentDataSpecified')}</div>;
}
return (
<AgentExecution
@@ -282,10 +284,10 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
case 'import-agent':
// TODO: Implement import agent component
return <div className="p-4">Import agent functionality coming soon...</div>;
return <div className="p-4">{t('messages.importAgentComingSoon')}</div>;
default:
return <div className="p-4">Unknown tab type: {tab.type}</div>;
return <div className="p-4">{t('messages.unknownTabType')}: {tab.type}</div>;
}
};
@@ -311,6 +313,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
};
export const TabContent: React.FC = () => {
const { t } = useTranslation();
const { tabs, activeTabId, createChatTab, findTabBySessionId, createClaudeFileTab, createAgentExecutionTab, createCreateAgentTab, createImportAgentTab, closeTab, updateTab } = useTabState();
// Listen for events to open sessions in tabs
@@ -415,8 +418,8 @@ export const TabContent: React.FC = () => {
{tabs.length === 0 && (
<div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center">
<p className="text-lg mb-2">No tabs open</p>
<p className="text-sm">Click the + button to start a new chat</p>
<p className="text-lg mb-2">{t('messages.noTabsOpen')}</p>
<p className="text-sm">{t('messages.clickPlusToStartChat')}</p>
</div>
</div>
)}

View File

@@ -17,6 +17,7 @@ import {
Briefcase
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
interface UsageDashboardProps {
/**
@@ -32,6 +33,7 @@ interface UsageDashboardProps {
* <UsageDashboard onBack={() => setView('welcome')} />
*/
export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [stats, setStats] = useState<UsageStats | null>(null);
@@ -82,7 +84,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
setSessionStats(sessionData);
} catch (err) {
console.error("Failed to load usage stats:", err);
setError("Failed to load usage statistics. Please try again.");
setError(t('usage.failedToLoadUsageStats'));
} finally {
setLoading(false);
}
@@ -146,9 +148,9 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-lg font-semibold">Usage Dashboard</h1>
<h1 className="text-lg font-semibold">{t('usage.usageDashboardTitle')}</h1>
<p className="text-xs text-muted-foreground">
Track your Claude Code usage and costs
{t('usage.trackUsageAndCosts')}
</p>
</div>
</div>
@@ -165,7 +167,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
onClick={() => setSelectedDateRange(range)}
className="text-xs"
>
{range === "all" ? "All Time" : range === "7d" ? "Last 7 Days" : "Last 30 Days"}
{range === "all" ? t('usage.allTime') : range === "7d" ? t('usage.last7Days') : t('usage.last30Days')}
</Button>
))}
</div>
@@ -179,7 +181,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground mx-auto mb-4" />
<p className="text-sm text-muted-foreground">Loading usage statistics...</p>
<p className="text-sm text-muted-foreground">{t('usage.loadingUsageStats')}</p>
</div>
</div>
) : error ? (
@@ -187,7 +189,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<div className="text-center max-w-md">
<p className="text-sm text-destructive mb-4">{error}</p>
<Button onClick={loadUsageStats} size="sm">
Try Again
{t('usage.tryAgain')}
</Button>
</div>
</div>
@@ -204,7 +206,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<Card className="p-4 shimmer-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">Total Cost</p>
<p className="text-xs text-muted-foreground">{t('usage.totalCost')}</p>
<p className="text-2xl font-bold mt-1">
{formatCurrency(stats.total_cost)}
</p>
@@ -217,7 +219,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<Card className="p-4 shimmer-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">Total Sessions</p>
<p className="text-xs text-muted-foreground">{t('usage.totalSessions')}</p>
<p className="text-2xl font-bold mt-1">
{formatNumber(stats.total_sessions)}
</p>
@@ -230,7 +232,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<Card className="p-4 shimmer-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">Total Tokens</p>
<p className="text-xs text-muted-foreground">{t('usage.totalTokens')}</p>
<p className="text-2xl font-bold mt-1">
{formatTokens(stats.total_tokens)}
</p>
@@ -243,7 +245,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<Card className="p-4 shimmer-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">Avg Cost/Session</p>
<p className="text-xs text-muted-foreground">{t('usage.avgCostPerSession')}</p>
<p className="text-2xl font-bold mt-1">
{formatCurrency(
stats.total_sessions > 0
@@ -260,32 +262,32 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{/* Tabs for different views */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="models">By Model</TabsTrigger>
<TabsTrigger value="projects">By Project</TabsTrigger>
<TabsTrigger value="sessions">By Session</TabsTrigger>
<TabsTrigger value="timeline">Timeline</TabsTrigger>
<TabsTrigger value="overview">{t('usage.overview')}</TabsTrigger>
<TabsTrigger value="models">{t('usage.byModel')}</TabsTrigger>
<TabsTrigger value="projects">{t('usage.byProject')}</TabsTrigger>
<TabsTrigger value="sessions">{t('usage.byDate')}</TabsTrigger>
<TabsTrigger value="timeline">{t('usage.timeline')}</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-4">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">Token Breakdown</h3>
<h3 className="text-sm font-semibold mb-4">{t('usage.tokenBreakdown')}</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-xs text-muted-foreground">Input Tokens</p>
<p className="text-xs text-muted-foreground">{t('usage.inputTokens')}</p>
<p className="text-lg font-semibold">{formatTokens(stats.total_input_tokens)}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Output Tokens</p>
<p className="text-xs text-muted-foreground">{t('usage.outputTokens')}</p>
<p className="text-lg font-semibold">{formatTokens(stats.total_output_tokens)}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Cache Write</p>
<p className="text-xs text-muted-foreground">{t('usage.cacheWrite')}</p>
<p className="text-lg font-semibold">{formatTokens(stats.total_cache_creation_tokens)}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Cache Read</p>
<p className="text-xs text-muted-foreground">{t('usage.cacheRead')}</p>
<p className="text-lg font-semibold">{formatTokens(stats.total_cache_read_tokens)}</p>
</div>
</div>
@@ -294,7 +296,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">Most Used Models</h3>
<h3 className="text-sm font-semibold mb-4">{t('usage.mostUsedModels')}</h3>
<div className="space-y-3">
{stats.by_model.slice(0, 3).map((model) => (
<div key={model.model} className="flex items-center justify-between">
@@ -303,7 +305,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{getModelDisplayName(model.model)}
</Badge>
<span className="text-xs text-muted-foreground">
{model.session_count} sessions
{model.session_count} {t('usage.sessions')}
</span>
</div>
<span className="text-sm font-medium">
@@ -315,7 +317,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</Card>
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">Top Projects</h3>
<h3 className="text-sm font-semibold mb-4">{t('usage.topProjects')}</h3>
<div className="space-y-3">
{stats.by_project.slice(0, 3).map((project) => (
<div key={project.project_path} className="flex items-center justify-between">
@@ -324,7 +326,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{project.project_path}
</span>
<span className="text-xs text-muted-foreground">
{project.session_count} sessions
{project.session_count} {t('usage.sessions')}
</span>
</div>
<span className="text-sm font-medium">
@@ -340,7 +342,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{/* Models Tab */}
<TabsContent value="models">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">Usage by Model</h3>
<h3 className="text-sm font-semibold mb-4">{t('usage.usageByModel')}</h3>
<div className="space-y-4">
{stats.by_model.map((model) => (
<div key={model.model} className="space-y-2">
@@ -353,7 +355,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{getModelDisplayName(model.model)}
</Badge>
<span className="text-sm text-muted-foreground">
{model.session_count} sessions
{model.session_count} {t('usage.sessions')}
</span>
</div>
<span className="text-sm font-semibold">
@@ -362,11 +364,11 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</div>
<div className="grid grid-cols-4 gap-2 text-xs">
<div>
<span className="text-muted-foreground">Input: </span>
<span className="text-muted-foreground">{t('usage.input')}: </span>
<span className="font-medium">{formatTokens(model.input_tokens)}</span>
</div>
<div>
<span className="text-muted-foreground">Output: </span>
<span className="text-muted-foreground">{t('usage.output')}: </span>
<span className="font-medium">{formatTokens(model.output_tokens)}</span>
</div>
<div>
@@ -387,7 +389,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{/* Projects Tab */}
<TabsContent value="projects">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">Usage by Project</h3>
<h3 className="text-sm font-semibold mb-4">{t('usage.usageByProject')}</h3>
<div className="space-y-3">
{stats.by_project.map((project) => (
<div key={project.project_path} className="flex items-center justify-between py-2 border-b border-border last:border-0">
@@ -397,17 +399,17 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</span>
<div className="flex items-center space-x-3 mt-1">
<span className="text-xs text-muted-foreground">
{project.session_count} sessions
{project.session_count} {t('usage.sessions')}
</span>
<span className="text-xs text-muted-foreground">
{formatTokens(project.total_tokens)} tokens
{formatTokens(project.total_tokens)} {t('usage.tokens')}
</span>
</div>
</div>
<div className="text-right">
<p className="text-sm font-semibold">{formatCurrency(project.total_cost)}</p>
<p className="text-xs text-muted-foreground">
{formatCurrency(project.total_cost / project.session_count)}/session
{formatCurrency(project.total_cost / project.session_count)}/{t('usage.session')}
</p>
</div>
</div>
@@ -419,7 +421,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{/* Sessions Tab */}
<TabsContent value="sessions">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">Usage by Session</h3>
<h3 className="text-sm font-semibold mb-4">{t('usage.usageBySession')}</h3>
<div className="space-y-3">
{sessionStats?.map((session) => (
<div key={`${session.project_path}-${session.project_name}`} className="flex items-center justify-between py-2 border-b border-border last:border-0">
@@ -451,7 +453,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<Card className="p-6">
<h3 className="text-sm font-semibold mb-6 flex items-center space-x-2">
<Calendar className="h-4 w-4" />
<span>Daily Usage</span>
<span>{t('usage.dailyUsage')}</span>
</h3>
{stats.by_date.length > 0 ? (() => {
const maxCost = Math.max(...stats.by_date.map(d => d.total_cost), 0);
@@ -484,13 +486,13 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<div className="bg-background border border-border rounded-lg shadow-lg p-3 whitespace-nowrap">
<p className="text-sm font-semibold">{formattedDate}</p>
<p className="text-sm text-muted-foreground mt-1">
Cost: {formatCurrency(day.total_cost)}
{t('usage.cost')}: {formatCurrency(day.total_cost)}
</p>
<p className="text-xs text-muted-foreground">
{formatTokens(day.total_tokens)} tokens
{formatTokens(day.total_tokens)} {t('usage.tokens')}
</p>
<p className="text-xs text-muted-foreground">
{day.models_used.length} model{day.models_used.length !== 1 ? 's' : ''}
{day.models_used.length} {t('usage.models')}{day.models_used.length !== 1 ? 's' : ''}
</p>
</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 -mt-1">
@@ -517,13 +519,13 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{/* X-axis label */}
<div className="mt-8 text-center text-xs text-muted-foreground">
Daily Usage Over Time
{t('usage.dailyUsageOverTime')}
</div>
</div>
)
})() : (
<div className="text-center py-8 text-sm text-muted-foreground">
No usage data available for the selected period
{t('usage.noUsageData')}
</div>
)}
</Card>

View File

@@ -1,4 +1,5 @@
import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';
import { useTranslation } from '@/hooks/useTranslation';
export interface Tab {
id: string;
@@ -37,6 +38,7 @@ const TabContext = createContext<TabContextType | undefined>(undefined);
const MAX_TABS = 20;
export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { t } = useTranslation();
const [tabs, setTabs] = useState<Tab[]>([]);
const [activeTabId, setActiveTabId] = useState<string | null>(null);
@@ -46,7 +48,7 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children
const defaultTab: Tab = {
id: generateTabId(),
type: 'projects',
title: 'CC Projects',
title: t('ccProjects'),
status: 'idle',
hasUnsavedChanges: false,
order: 0,
@@ -55,7 +57,7 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children
};
setTabs([defaultTab]);
setActiveTabId(defaultTab.id);
}, []);
}, [t]);
// Tab persistence disabled - no longer saving to localStorage
// useEffect(() => {
@@ -75,7 +77,7 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children
const addTab = useCallback((tabData: Omit<Tab, 'id' | 'order' | 'createdAt' | 'updatedAt'>): string => {
if (tabs.length >= MAX_TABS) {
throw new Error(`Maximum number of tabs (${MAX_TABS}) reached`);
throw new Error(t('maximumTabsReached', { max: MAX_TABS }));
}
const newTab: Tab = {
@@ -89,7 +91,7 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children
setTabs(prevTabs => [...prevTabs, newTab]);
setActiveTabId(newTab.id);
return newTab.id;
}, [tabs.length]);
}, [tabs.length, t]);
const removeTab = useCallback((id: string) => {
setTabs(prevTabs => {

View File

@@ -1,6 +1,7 @@
import { useCallback, useMemo } from 'react';
import { useTabContext } from '@/contexts/TabContext';
import { Tab } from '@/contexts/TabContext';
import { useTranslation } from '@/hooks/useTranslation';
interface UseTabStateReturn {
// State
@@ -40,6 +41,7 @@ interface UseTabStateReturn {
}
export const useTabState = (): UseTabStateReturn => {
const { t } = useTranslation();
const {
tabs,
activeTabId,
@@ -100,12 +102,12 @@ export const useTabState = (): UseTabStateReturn => {
return addTab({
type: 'projects',
title: 'CC Projects',
title: t('ccProjects'),
status: 'idle',
hasUnsavedChanges: false,
icon: 'folder'
});
}, [addTab, tabs, setActiveTab]);
}, [addTab, tabs, setActiveTab, t]);
const createUsageTab = useCallback((): string | null => {
// Check if usage tab already exists (singleton)
@@ -117,12 +119,12 @@ export const useTabState = (): UseTabStateReturn => {
return addTab({
type: 'usage',
title: 'Usage',
title: t('messages.usage'),
status: 'idle',
hasUnsavedChanges: false,
icon: 'bar-chart'
});
}, [addTab, tabs, setActiveTab]);
}, [addTab, tabs, setActiveTab, t]);
const createMCPTab = useCallback((): string | null => {
// Check if MCP tab already exists (singleton)
@@ -134,12 +136,12 @@ export const useTabState = (): UseTabStateReturn => {
return addTab({
type: 'mcp',
title: 'MCP Servers',
title: t('messages.mcpServersTitle'),
status: 'idle',
hasUnsavedChanges: false,
icon: 'server'
});
}, [addTab, tabs, setActiveTab]);
}, [addTab, tabs, setActiveTab, t]);
const createSettingsTab = useCallback((): string | null => {
// Check if settings tab already exists (singleton)
@@ -151,12 +153,12 @@ export const useTabState = (): UseTabStateReturn => {
return addTab({
type: 'settings',
title: 'Settings',
title: t('settings.title'),
status: 'idle',
hasUnsavedChanges: false,
icon: 'settings'
});
}, [addTab, tabs, setActiveTab]);
}, [addTab, tabs, setActiveTab, t]);
const createClaudeMdTab = useCallback((): string | null => {
// Check if claude-md tab already exists (singleton)
@@ -168,12 +170,12 @@ export const useTabState = (): UseTabStateReturn => {
return addTab({
type: 'claude-md',
title: 'CLAUDE.md',
title: t('messages.claudemdTitle'),
status: 'idle',
hasUnsavedChanges: false,
icon: 'file-text'
});
}, [addTab, tabs, setActiveTab]);
}, [addTab, tabs, setActiveTab, t]);
const createClaudeFileTab = useCallback((fileId: string, fileName: string): string => {
// Check if tab already exists for this file
@@ -196,13 +198,13 @@ export const useTabState = (): UseTabStateReturn => {
const createAgentExecutionTab = useCallback((agent: any, _tabId: string): string => {
return addTab({
type: 'agent-execution',
title: `Run: ${agent.name}`,
title: t('messages.runAgent', { name: agent.name }),
agentData: agent,
status: 'idle',
hasUnsavedChanges: false,
icon: 'bot'
});
}, [addTab]);
}, [addTab, t]);
const createCreateAgentTab = useCallback((): string => {
// Check if create agent tab already exists (singleton)
@@ -214,12 +216,12 @@ export const useTabState = (): UseTabStateReturn => {
return addTab({
type: 'create-agent',
title: 'Create Agent',
title: t('messages.createAgent'),
status: 'idle',
hasUnsavedChanges: false,
icon: 'plus'
});
}, [addTab, tabs, setActiveTab]);
}, [addTab, tabs, setActiveTab, t]);
const createImportAgentTab = useCallback((): string => {
// Check if import agent tab already exists (singleton)
@@ -231,12 +233,12 @@ export const useTabState = (): UseTabStateReturn => {
return addTab({
type: 'import-agent',
title: 'Import Agent',
title: t('messages.importAgent'),
status: 'idle',
hasUnsavedChanges: false,
icon: 'import'
});
}, [addTab, tabs, setActiveTab]);
}, [addTab, tabs, setActiveTab, t]);
const closeTab = useCallback(async (id: string, force: boolean = false): Promise<boolean> => {
const tab = getTabById(id);
@@ -245,13 +247,13 @@ export const useTabState = (): UseTabStateReturn => {
// Check for unsaved changes
if (!force && tab.hasUnsavedChanges) {
// In a real implementation, you'd show a confirmation dialog here
const confirmed = window.confirm(`Tab "${tab.title}" has unsaved changes. Close anyway?`);
const confirmed = window.confirm(t('messages.unsavedChangesCloseConfirm', { title: tab.title }));
if (!confirmed) return false;
}
removeTab(id);
return true;
}, [getTabById, removeTab]);
}, [getTabById, removeTab, t]);
const closeCurrentTab = useCallback(async (): Promise<boolean> => {
if (!activeTabId) return true;

View File

@@ -1,4 +1,10 @@
{
"ccProjects": "CC Projects",
"browseClaudeCodeSessions": "Browse your Claude Code sessions",
"newClaudeCodeSession": "New Claude Code session",
"ccAgents": "CC Agents",
"mcpServers": "MCP Servers",
"manageMcpServers": "Manage Model Context Protocol servers",
"app": {
"name": "Claudia",
"welcome": "Welcome to Claudia",
@@ -19,7 +25,12 @@
"previous": "Previous",
"refresh": "Refresh",
"close": "Close",
"open": "Open"
"open": "Open",
"page": "Page",
"of": "of",
"loading": "Loading...",
"from": "from",
"retry": "Retry"
},
"navigation": {
"projects": "CC Projects",
@@ -47,9 +58,10 @@
"editAgent": "Edit Agent",
"deleteAgent": "Delete Agent",
"executeAgent": "Execute Agent",
"agentName": "Agent Name",
"agentNameRequired": "Agent name is required",
"agentIcon": "Agent Icon",
"systemPrompt": "System Prompt",
"systemPromptRequired": "System prompt is required",
"defaultTask": "Default Task",
"model": "Model",
"permissions": "Permissions",
@@ -66,7 +78,60 @@
"createAgentDescription": "Create a new Claude Code agent",
"updateAgentDescription": "Update your Claude Code agent",
"createFailed": "Failed to create agent",
"updateFailed": "Failed to update agent"
"updateFailed": "Failed to update agent",
"basicInformation": "Basic Information",
"optional": "Optional",
"sonnetDescription": "Faster, efficient for most tasks",
"opusDescription": "More capable, better for complex tasks",
"defaultTaskDescription": "This will be used as the default task placeholder when executing the agent",
"systemPromptDescription": "Define the behavior and capabilities of your CC Agent",
"manageAgents": "Manage your Claude Code agents",
"noAgentsYet": "No agents yet",
"createFirstAgent": "Create your first CC Agent to get started",
"created": "Created",
"execute": "Execute",
"export": "Export",
"import": "Import",
"importFromFile": "From File",
"importFromGitHub": "From GitHub",
"recentExecutions": "Recent Executions",
"exportedSuccessfully": "Agent \"{{name}}\" exported successfully",
"exportFailed": "Failed to export agent",
"importedSuccessfully": "Agent imported successfully",
"importFailed": "Failed to import agent",
"importFromGitHubSuccess": "Agent imported successfully from GitHub",
"executeAgentTitle": "Execute agent",
"deleteAgentTitle": "Delete Agent",
"deleteConfirmation": "Are you sure you want to delete the agent \"{{name}}\"? This action cannot be undone and will permanently remove the agent and all its associated data.",
"deleting": "Deleting...",
"deleteAgentButton": "Delete Agent",
"agentManagement": "Agent Management",
"createNewOrManageAgents": "Create new agents or manage running agent executions",
"noAgentsAvailable": "No agents available",
"availableAgents": "Available Agents",
"runningAgents": "Running Agents",
"createFirstAgentToGetStarted": "Create your first agent to get started"
},
"slashCommands": {
"slashCommands": "Slash Commands",
"projectSlashCommands": "Project Slash Commands",
"createCustomCommandsProject": "Create custom commands for this project",
"createCustomCommandsWorkflow": "Create custom commands to streamline your workflow",
"newCommand": "New Command",
"allCommands": "All Commands",
"project": "Project",
"user": "User",
"noCommandsFound": "No commands found",
"noProjectCommandsYet": "No project commands created yet",
"noCommandsYet": "No commands created yet",
"createFirstProjectCommand": "Create your first project command",
"createFirstCommand": "Create your first command"
},
"hooks": {
"hooksConfiguration": "Hooks Configuration",
"configureShellCommands": "Configure shell commands to execute at various points in Claude Code's lifecycle.",
"localSettingsNote": " These settings are not committed to version control.",
"unsavedChanges": "You have unsaved changes. Click Save to persist them."
},
"settings": {
"title": "Settings",
@@ -85,7 +150,142 @@
"noProxy": "No Proxy",
"analyticsConsent": "Analytics Consent",
"enableAnalytics": "Enable Analytics",
"disableAnalytics": "Disable Analytics"
"disableAnalytics": "Disable Analytics",
"configurePreferences": "Configure Claude Code preferences",
"generalSettings": "General Settings",
"permissionsTab": "Permissions",
"environmentTab": "Environment",
"advancedTab": "Advanced",
"hooksTab": "Hooks",
"commands": "Commands",
"storage": "Storage",
"proxy": "Proxy",
"analyticsTab": "Analytics",
"proxySettingsTitle": "Proxy Settings",
"proxySettingsDesc": "Configure proxy settings for Claude API requests",
"enableProxyDesc": "Use proxy for all Claude API requests",
"noProxyDesc": "Comma-separated list of hosts that should bypass the proxy",
"allProxyDesc": "Proxy URL to use for all protocols if protocol-specific proxies are not set",
"proxySettingsSaved": "Proxy settings saved and applied successfully.",
"loadProxySettingsFailed": "Failed to load proxy settings",
"saveProxySettingsFailed": "Failed to save proxy settings",
"themeSelector": {
"selectATheme": "Select a theme",
"dark": "Dark",
"gray": "Gray",
"light": "Light",
"custom": "Custom",
"choosePreferredTheme": "Choose your preferred color theme for the interface"
},
"customTheme": {
"title": "Custom Theme Colors",
"background": "Background",
"foreground": "Foreground",
"primary": "Primary",
"card": "Card",
"accent": "Accent",
"destructive": "Destructive",
"colorValuesDesc": "Use CSS color values (hex, rgb, oklch, etc.). Changes apply immediately."
},
"generalOptions": {
"includeCoAuthor": "Include \"Co-authored by Claude\"",
"includeCoAuthorDesc": "Add Claude attribution to git commits and pull requests",
"verboseOutput": "Verbose Output",
"verboseOutputDesc": "Show full bash and command outputs",
"chatRetention": "Chat Transcript Retention (days)",
"chatRetentionDesc": "How long to retain chat transcripts locally (default: 30 days)",
"claudeCodeInstallation": "Claude Code Installation",
"choosePreferredInstallation": "Choose your preferred Claude Code installation.",
"loadingAvailableInstallations": "Loading available installations...",
"errorLoadingInstallations": "Error loading installations",
"availableInstallations": "Available Installations",
"claudeCodeInstallationDesc": "Select which Claude Code installation to use.",
"binaryPathChanged": "⚠️ Claude binary path has been changed. Remember to save your settings."
},
"permissions": {
"permissionRules": "Permission Rules",
"permissionRulesDesc": "Control which tools Claude Code can use without manual approval",
"allowRules": "Allow Rules",
"denyRules": "Deny Rules",
"addRule": "Add Rule",
"noAllowRules": "No allow rules configured. Claude will ask for approval for all tools.",
"noDenyRules": "No deny rules configured.",
"examples": "Examples:",
"exampleBash": "Allow all bash commands",
"exampleExactCommand": "Allow exact command",
"examplePrefix": "Allow commands with prefix",
"exampleReadFile": "Allow reading specific file",
"exampleEditDir": "Allow editing files in docs directory"
},
"environment": {
"environmentVariables": "Environment Variables",
"environmentVariablesDesc": "Environment variables applied to every Claude Code session",
"addVariable": "Add Variable",
"noEnvironmentVariables": "No environment variables configured.",
"commonVariables": "Common variables:",
"telemetryDesc": "Enable/disable telemetry (0 or 1)",
"modelDesc": "Custom model name",
"costWarningsDesc": "Disable cost warnings (1)"
},
"advanced": {
"advancedSettings": "Advanced Settings",
"advancedSettingsDesc": "Additional configuration options for advanced users",
"apiKeyHelper": "API Key Helper Script",
"apiKeyHelperDesc": "Custom script to generate auth values for API requests",
"rawSettings": "Raw Settings (JSON)",
"rawSettingsDesc": "This shows the raw JSON that will be saved to ~/.claude/settings.json"
},
"hooks": {
"userHooks": "User Hooks",
"userHooksDesc": "Configure hooks that apply to all Claude Code sessions for your user account. These are stored in ~/.claude/settings.json"
},
"analytics": {
"analyticsSettings": "Analytics Settings",
"enableAnalytics": "Enable Analytics",
"enableAnalyticsDesc": "Help improve Claudia by sharing anonymous usage data",
"privacyProtected": "Your privacy is protected",
"noPersonalInfo": "No personal information is collected",
"noFileContents": "No file contents, paths, or project names",
"anonymousData": "All data is anonymous with random IDs",
"canDisable": "You can disable analytics at any time",
"whatWeCollect": "What we collect:",
"featureUsage": "Feature usage patterns",
"performanceMetrics": "Performance metrics",
"errorReports": "Error reports (without sensitive data)",
"sessionFrequency": "Session frequency and duration",
"deleteAllData": "Delete All Analytics Data",
"analyticsEnabled": "Analytics enabled",
"analyticsDisabled": "Analytics disabled",
"allDataDeleted": "All analytics data deleted",
"helpImproveClaudia": "Help Improve Claudia",
"collectAnonymousData": "We'd like to collect anonymous usage data to improve your experience.",
"featureUsageDesc": "Feature usage (which tools and commands you use)",
"performanceMetricsDesc": "Performance metrics (app speed and reliability)",
"errorReportsDesc": "Error reports (to fix bugs and improve stability)",
"usagePatternsDesc": "General usage patterns (session frequency and duration)",
"noApiKeys": "No API keys or sensitive data",
"canOptOut": "You can opt-out anytime in Settings",
"dataHelpsUs": "This data helps us understand which features are most valuable, identify performance issues, and prioritize improvements. Your choice won't affect any functionality.",
"noThanks": "No Thanks",
"allowAnalytics": "Allow Analytics"
},
"saveButton": {
"saving": "Saving...",
"saveSettings": "Save Settings",
"settingsSavedSuccess": "Settings saved successfully!",
"settingsSaveFailed": "Failed to save settings"
},
"messages": {
"loadFailed": "Failed to load settings. Please ensure ~/.claude directory exists.",
"saveFailed": "Failed to save settings."
},
"placeholders": {
"envVarKey": "KEY",
"envVarValue": "value",
"allowRuleExample": "e.g., Bash(npm run test:*)",
"denyRuleExample": "e.g., Bash(curl:*)",
"apiKeyHelperPath": "/path/to/generate_api_key.sh"
}
},
"mcp": {
"title": "MCP Server Management",
@@ -110,7 +310,118 @@
"last7Days": "Last 7 Days",
"last30Days": "Last 30 Days",
"allTime": "All Time",
"exportData": "Export Data"
"exportData": "Export Data",
"usageDashboardTitle": "Usage Dashboard",
"trackUsageAndCosts": "Track your Claude Code usage and costs",
"allTime": "All Time",
"totalCost": "Total Cost",
"totalSessions": "Total Sessions",
"totalTokens": "Total Tokens",
"avgCostPerSession": "Avg Cost/Session",
"overview": "Overview",
"tokenBreakdown": "Token Breakdown",
"inputTokens": "Input Tokens",
"outputTokens": "Output Tokens",
"cacheWrite": "Cache Write",
"cacheRead": "Cache Read",
"mostUsedModels": "Most Used Models",
"topProjects": "Top Projects",
"sessions": "sessions",
"usageByModel": "Usage by Model",
"usageByProject": "Usage by Project",
"usageBySession": "Usage by Session",
"timeline": "Timeline",
"dailyUsage": "Daily Usage",
"tokens": "tokens",
"models": "models",
"input": "Input",
"output": "Output",
"session": "session",
"loadingUsageStats": "Loading usage statistics...",
"failedToLoadUsageStats": "Failed to load usage statistics. Please try again.",
"tryAgain": "Try Again",
"dailyUsageOverTime": "Daily Usage Over Time",
"noUsageData": "No usage data available for the selected period",
"cost": "Cost",
"lastUsed": "Last Used",
"markdownEditorTitle": "Markdown Editor",
"editSystemPrompt": "Edit your Claude Code system prompt",
"claudemdFile": "CLAUDE.md file",
"loadClaudemdFailed": "Failed to load CLAUDE.md file",
"saveClaudemdFailed": "Failed to save CLAUDE.md file",
"claudemdSavedSuccess": "CLAUDE.md saved successfully",
"saveClaudemd": "Save CLAUDE.md",
"unsavedChangesConfirm": "You have unsaved changes. Are you sure you want to leave?",
"servers": "Servers",
"addServer": "Add Server",
"importExport": "Import/Export",
"mcpServerAdded": "MCP server added successfully!",
"serverRemovedSuccess": "Server \"{{name}}\" removed successfully!",
"importedServersSuccess": "Successfully imported {{count}} server{{plural}}!",
"importedServersFailed": "Imported {{imported}} server{{importedPlural}}, {{failed}} failed",
"loadMcpServersFailed": "Failed to load MCP servers. Make sure Claude Code is installed.",
"addMcpServer": "Add MCP Server",
"configureNewMcpServer": "Configure a new Model Context Protocol server",
"serverNameRequired": "Server name is required",
"commandRequired": "Command is required",
"urlRequired": "URL is required",
"failedToAddServer": "Failed to add server",
"environmentVariables": "Environment Variables",
"addVariable": "Add Variable",
"serverName": "Server Name",
"command": "Command",
"argumentsOptional": "Arguments (optional)",
"scope": "Scope",
"localProjectOnly": "Local (this project only)",
"projectSharedViaMcp": "Project (shared via .mcp.json)",
"userAllProjects": "User (all projects)",
"addingServer": "Adding Server...",
"addStdioServer": "Add Stdio Server",
"url": "URL",
"addSseServer": "Add SSE Server",
"exampleCommands": "Example Commands",
"uniqueNameToIdentify": "A unique name to identify this server",
"commandToExecuteServer": "The command to execute the server",
"spaceSeparatedArgs": "Space-separated command arguments",
"sseEndpointUrl": "The SSE endpoint URL",
"running": "Running",
"showFull": "Show full",
"hide": "Hide",
"copied": "Copied!",
"copy": "Copy",
"arguments": "Arguments",
"environmentVariables": "Environment Variables",
"configuredServers": "Configured Servers",
"serversConfigured": "configured",
"refresh": "Refresh",
"noMcpServersConfigured": "No MCP servers configured",
"addServerToGetStarted": "Add a server to get started with Model Context Protocol",
"localProjectSpecific": "Local (Project-specific)",
"projectSharedMcp": "Project (Shared via .mcp.json)",
"userAllProjects": "User (All projects)",
"welcomeToClaudia": "Welcome to Claudia",
"backToHome": "Back to Home",
"noProjectsFound": "No projects found in ~/.claude/projects",
"failedToLoadProjects": "Failed to load projects. Please ensure ~/.claude directory exists.",
"failedToLoadSessions": "Failed to load sessions for this project.",
"claudeBinaryPathSaved": "Claude binary path saved successfully",
"selectClaudeCodeInstallation": "Select Claude Code Installation",
"multipleInstallationsFound": "Multiple Claude Code installations were found on your system. Please select which one you'd like to use.",
"claudeCodeNotFoundDialog": "Claude Code was not found in any of the common installation locations. Please install Claude Code to continue.",
"searchedLocations": "Searched locations",
"installationTip": "You can install Claude Code using",
"searchingInstallations": "Searching for Claude installations...",
"installationGuide": "Installation Guide",
"validating": "Validating...",
"saveSelection": "Save Selection",
"noInstallationsFound": "No Installations Found",
"pleaseSelectInstallation": "Please select a Claude installation",
"fileSavedSuccess": "File saved successfully",
"saveFileFailed": "Failed to save file",
"loadFileFailed": "Failed to load CLAUDE.md file",
"editProjectSpecificPrompt": "Edit project-specific Claude Code system prompt",
"maximumTabsReached": "Maximum number of tabs ({{max}}) reached",
"saving": "Saving..."
},
"checkpoint": {
"title": "Checkpoints",
@@ -121,14 +432,39 @@
"checkpointMessage": "Checkpoint Message",
"timeline": "Timeline",
"diff": "Diff",
"noCheckpoints": "No checkpoints found"
"noCheckpoints": "No checkpoints found",
"checkpointSettingsTitle": "Checkpoint Settings",
"experimentalFeature": "Experimental Feature",
"checkpointWarning": "Checkpointing may affect directory structure or cause data loss. Use with caution.",
"automaticCheckpoints": "Automatic Checkpoints",
"automaticCheckpointsDesc": "Automatically create checkpoints based on the selected strategy",
"checkpointStrategy": "Checkpoint Strategy",
"manualOnly": "Manual Only",
"afterEachPrompt": "After Each Prompt",
"afterToolUse": "After Tool Use",
"smart": "Smart (Recommended)",
"manualOnlyDesc": "Checkpoints will only be created manually",
"afterEachPromptDesc": "A checkpoint will be created after each user prompt",
"afterToolUseDesc": "A checkpoint will be created after each tool use",
"smartDesc": "Checkpoints will be created after destructive operations",
"saving": "Saving...",
"saveSettings": "Save Settings",
"storageManagement": "Storage Management",
"totalCheckpoints": "Total checkpoints",
"keepRecentCheckpoints": "Keep Recent Checkpoints",
"cleanUp": "Clean Up",
"removeOldCheckpoints": "Remove old checkpoints, keeping only the most recent {{count}}",
"checkpointSettingsFailed": "Failed to load checkpoint settings",
"saveCheckpointSettingsFailed": "Failed to save checkpoint settings",
"cleanupCheckpointsFailed": "Failed to cleanup checkpoints",
"removedOldCheckpoints": "Removed {{count}} old checkpoints"
},
"placeholders": {
"searchProjects": "Search projects...",
"searchAgents": "Search agents...",
"enterAgentName": "Enter agent name...",
"enterAgentName": "e.g., Code Assistant",
"enterSystemPrompt": "Enter system prompt...",
"enterDefaultTask": "Enter default task...",
"enterDefaultTask": "e.g., Review this code for security issues",
"enterURL": "Enter URL...",
"searchCommands": "Search commands...",
"enterCommand": "Enter command...",
@@ -151,6 +487,34 @@
"unknownError": "Unknown error occurred",
"claudeCodeNotFound": "Claude Code not found",
"selectClaudeInstallation": "Select Claude Installation",
"installClaudeCode": "Install Claude Code"
"installClaudeCode": "Install Claude Code",
"noTabsOpen": "No tabs open",
"clickPlusToStartChat": "Click the + button to start a new chat",
"noAgentRunIdSpecified": "No agent run ID specified",
"noClaudeFileIdSpecified": "No Claude file ID specified",
"claudeFileEditorNotImplemented": "Claude file editor not yet implemented in tabs",
"noAgentDataSpecified": "No agent data specified",
"importAgentComingSoon": "Import agent functionality coming soon...",
"unknownTabType": "Unknown tab type",
"letClaudeDecide": "Let Claude decide",
"basicReasoning": "Basic reasoning",
"deeperAnalysis": "Deeper analysis",
"extensiveReasoning": "Extensive reasoning",
"maximumAnalysis": "Maximum analysis",
"typeYourPromptHere": "Type your prompt here...",
"dropImagesHere": "Drop images here...",
"askClaudeAnything": "Ask Claude anything...",
"usage": "Usage",
"mcpServersTitle": "MCP Servers",
"claudemdTitle": "CLAUDE.md",
"runAgent": "Run: {{name}}",
"createAgent": "Create Agent",
"importAgent": "Import Agent",
"unsavedChangesCloseConfirm": "Tab \"{{title}}\" has unsaved changes. Close anyway?",
"session": "Session"
},
"input": {
"pressEnterToSend": "Press Enter to send, Shift+Enter for new line",
"withFileAndCommandSupport": ", @ to mention files, / for commands, drag & drop or paste images"
}
}

View File

@@ -1,4 +1,10 @@
{
"ccProjects": "Claude Code 项目",
"browseClaudeCodeSessions": "浏览您的 Claude Code 会话",
"newClaudeCodeSession": "新建 Claude Code 会话",
"ccAgents": "CC 智能体",
"mcpServers": "MCP 服务器",
"manageMcpServers": "管理模型上下文协议服务器",
"app": {
"name": "Claudia",
"welcome": "欢迎使用 Claudia",
@@ -18,11 +24,13 @@
"next": "下一步",
"previous": "上一步",
"refresh": "刷新",
"close": "关闭",
"open": "打开"
"page": "页面",
"of": "共",
"loading": "加载中...",
"from": "从"
},
"navigation": {
"projects": "CC 项目",
"projects": "Claude Code 项目",
"agents": "CC 智能体",
"settings": "设置",
"usage": "用量仪表板",
@@ -47,9 +55,10 @@
"editAgent": "编辑智能体",
"deleteAgent": "删除智能体",
"executeAgent": "执行智能体",
"agentName": "智能体名称",
"agentNameRequired": "代理名称为必填项",
"agentIcon": "智能体图标",
"systemPrompt": "系统提示",
"systemPromptRequired": "系统提示为必填项",
"defaultTask": "默认任务",
"model": "模型",
"permissions": "权限",
@@ -66,7 +75,60 @@
"createAgentDescription": "创建新的 Claude Code 智能体",
"updateAgentDescription": "更新您的 Claude Code 智能体",
"createFailed": "创建智能体失败",
"updateFailed": "更新智能体失败"
"updateFailed": "更新智能体失败",
"basicInformation": "基本信息",
"optional": "可选",
"sonnetDescription": "更快,适用于大多数任务",
"opusDescription": "功能更强,适用于复杂任务",
"defaultTaskDescription": "执行智能体时将用作默认任务占位符",
"systemPromptDescription": "定义您的 CC 智能体的行为和功能",
"manageAgents": "管理您的 Claude Code 智能体",
"noAgentsYet": "还没有智能体",
"createFirstAgent": "创建您的第一个 CC 智能体来开始",
"created": "创建于",
"execute": "执行",
"export": "导出",
"import": "导入",
"importFromFile": "从文件导入",
"importFromGitHub": "从 GitHub 导入",
"recentExecutions": "最近执行记录",
"exportedSuccessfully": "智能体 \"{{name}}\" 导出成功",
"exportFailed": "导出智能体失败",
"importedSuccessfully": "智能体导入成功",
"importFailed": "导入智能体失败",
"importFromGitHubSuccess": "从 GitHub 导入智能体成功",
"executeAgentTitle": "执行智能体",
"deleteAgentTitle": "删除智能体",
"deleteConfirmation": "确定要删除智能体 \"{{name}}\" 吗?此操作无法撤销,将永久删除智能体及其所有相关数据。",
"deleting": "删除中...",
"deleteAgentButton": "删除智能体",
"agentManagement": "代理管理",
"createNewOrManageAgents": "创建新代理或管理运行中的代理执行",
"noAgentsAvailable": "无可用代理",
"availableAgents": "可用代理",
"runningAgents": "运行中的代理",
"createFirstAgentToGetStarted": "创建您的第一个代理开始使用"
},
"slashCommands": {
"slashCommands": "斜杠命令",
"projectSlashCommands": "项目斜杠命令",
"createCustomCommandsProject": "为此项目创建自定义命令",
"createCustomCommandsWorkflow": "创建自定义命令来简化您的工作流程",
"newCommand": "新建命令",
"allCommands": "所有命令",
"project": "项目",
"user": "用户",
"noCommandsFound": "未找到命令",
"noProjectCommandsYet": "尚未创建项目命令",
"noCommandsYet": "尚未创建命令",
"createFirstProjectCommand": "创建您的第一个项目命令",
"createFirstCommand": "创建您的第一个命令"
},
"hooks": {
"hooksConfiguration": "钩子配置",
"configureShellCommands": "配置在 Claude Code 生命周期的各个阶段执行的 shell 命令。",
"localSettingsNote": " 这些设置不会提交到版本控制中。",
"unsavedChanges": "您有未保存的更改。点击保存以持久化它们。"
},
"settings": {
"title": "设置",
@@ -85,7 +147,142 @@
"noProxy": "无代理",
"analyticsConsent": "分析同意",
"enableAnalytics": "启用分析",
"disableAnalytics": "禁用分析"
"disableAnalytics": "禁用分析",
"configurePreferences": "配置 Claude Code 偏好设置",
"generalSettings": "常规设置",
"permissionsTab": "权限",
"environmentTab": "环境",
"advancedTab": "高级",
"hooksTab": "钩子",
"commands": "命令",
"storage": "存储",
"proxy": "代理",
"analyticsTab": "分析",
"proxySettingsTitle": "代理设置",
"proxySettingsDesc": "为 Claude API 请求配置代理设置",
"enableProxyDesc": "为所有 Claude API 请求使用代理",
"noProxyDesc": "逗号分隔的主机列表,这些主机应绕过代理",
"allProxyDesc": "如果未设置协议特定代理,则用于所有协议的代理 URL",
"proxySettingsSaved": "代理设置已保存并应用成功。",
"loadProxySettingsFailed": "加载代理设置失败",
"saveProxySettingsFailed": "保存代理设置失败",
"themeSelector": {
"selectATheme": "选择主题",
"dark": "暗色",
"gray": "灰色",
"light": "浅色",
"custom": "自定义",
"choosePreferredTheme": "选择您偏好的界面配色主题"
},
"customTheme": {
"title": "自定义主题配色",
"background": "背景色",
"foreground": "前景色",
"primary": "主色",
"card": "卡片色",
"accent": "强调色",
"destructive": "危险色",
"colorValuesDesc": "使用 CSS 颜色值 (hex、rgb、oklch 等)。更改将立即生效。"
},
"generalOptions": {
"includeCoAuthor": "包含 \"由 Claude 协作\" 标识",
"includeCoAuthorDesc": "在 git 提交和拉取请求中添加 Claude 归属标识",
"verboseOutput": "详细输出",
"verboseOutputDesc": "显示完整的 bash 和命令输出",
"chatRetention": "聊天记录保留期 (天)",
"chatRetentionDesc": "本地保留聊天记录的时长默认30 天)",
"claudeCodeInstallation": "Claude Code 安装",
"choosePreferredInstallation": "选择您偏好的 Claude Code 安装。",
"loadingAvailableInstallations": "正在加载可用安装...",
"errorLoadingInstallations": "加载安装时出错",
"availableInstallations": "可用安装",
"claudeCodeInstallationDesc": "选择要使用的 Claude Code 安装。",
"binaryPathChanged": "⚠️ Claude 二进制路径已更改。请记住保存您的设置。"
},
"permissions": {
"permissionRules": "权限规则",
"permissionRulesDesc": "控制 Claude Code 可以使用哪些工具而无需手动批准",
"allowRules": "允许规则",
"denyRules": "拒绝规则",
"addRule": "添加规则",
"noAllowRules": "未配置允许规则。Claude 将请求批准所有工具。",
"noDenyRules": "未配置拒绝规则。",
"examples": "示例:",
"exampleBash": "允许所有 bash 命令",
"exampleExactCommand": "允许精确命令",
"examplePrefix": "允许带前缀的命令",
"exampleReadFile": "允许读取指定文件",
"exampleEditDir": "允许编辑 docs 目录中的文件"
},
"environment": {
"environmentVariables": "环境变量",
"environmentVariablesDesc": "应用于每个 Claude Code 会话的环境变量",
"addVariable": "添加变量",
"noEnvironmentVariables": "未配置环境变量。",
"commonVariables": "常用变量:",
"telemetryDesc": "启用/禁用遥测 (0 或 1)",
"modelDesc": "自定义模型名称",
"costWarningsDesc": "禁用成本警告 (1)"
},
"advanced": {
"advancedSettings": "高级设置",
"advancedSettingsDesc": "面向高级用户的额外配置选项",
"apiKeyHelper": "API 密钥辅助脚本",
"apiKeyHelperDesc": "为 API 请求生成认证值的自定义脚本",
"rawSettings": "原始设置 (JSON)",
"rawSettingsDesc": "这显示了将保存到 ~/.claude/settings.json 的原始 JSON 数据"
},
"hooks": {
"userHooks": "用户钩子",
"userHooksDesc": "配置适用于您用户账户所有 Claude Code 会话的钩子。这些存储在 ~/.claude/settings.json 中"
},
"analytics": {
"analyticsSettings": "分析设置",
"enableAnalytics": "启用分析",
"enableAnalyticsDesc": "通过分享匿名使用数据帮助改善 Claudia",
"privacyProtected": "您的隐私受到保护",
"noPersonalInfo": "不收集个人信息",
"noFileContents": "不收集文件内容、路径或项目名称",
"anonymousData": "所有数据都是匿名的,使用随机 ID",
"canDisable": "您可以随时禁用分析",
"whatWeCollect": "我们收集什么:",
"featureUsage": "功能使用模式",
"performanceMetrics": "性能指标",
"errorReports": "错误报告(不包含敏感数据)",
"sessionFrequency": "会话频率和时长",
"deleteAllData": "删除所有分析数据",
"analyticsEnabled": "分析已启用",
"analyticsDisabled": "分析已禁用",
"allDataDeleted": "所有分析数据已删除",
"helpImproveClaudia": "帮助改善 Claudia",
"collectAnonymousData": "我们希望收集匿名使用数据以改善您的使用体验。",
"featureUsageDesc": "功能使用(您使用的工具和命令)",
"performanceMetricsDesc": "性能指标(应用速度和可靠性)",
"errorReportsDesc": "错误报告(用于修复错误和提高稳定性)",
"usagePatternsDesc": "一般使用模式(会话频率和持续时间)",
"noApiKeys": "不收集 API 密钥或敏感数据",
"canOptOut": "您可以随时在设置中选择退出",
"dataHelpsUs": "这些数据有助于我们了解哪些功能最有价值,识别性能问题,并优先进行改进。您的选择不会影响任何功能。",
"noThanks": "不用了",
"allowAnalytics": "允许分析"
},
"saveButton": {
"saving": "保存中...",
"saveSettings": "保存设置",
"settingsSavedSuccess": "设置保存成功!",
"settingsSaveFailed": "保存设置失败"
},
"messages": {
"loadFailed": "加载设置失败。请确保 ~/.claude 目录存在。",
"saveFailed": "保存设置失败。"
},
"placeholders": {
"envVarKey": "键名",
"envVarValue": "值",
"allowRuleExample": "例如Bash(npm run test:*)",
"denyRuleExample": "例如Bash(curl:*)",
"apiKeyHelperPath": "/path/to/generate_api_key.sh"
}
},
"mcp": {
"title": "MCP 服务器管理",
@@ -110,7 +307,140 @@
"last7Days": "最近 7 天",
"last30Days": "最近 30 天",
"allTime": "全部时间",
"exportData": "导出数据"
"exportData": "导出数据",
"usageDashboardTitle": "用量仪表板",
"trackUsageAndCosts": "跟踪您的 Claude Code 用量和成本",
"allTime": "全部时间",
"totalCost": "总成本",
"totalSessions": "总会话数",
"totalTokens": "总令牌数",
"avgCostPerSession": "平均每会话成本",
"overview": "概览",
"tokenBreakdown": "令牌明细",
"inputTokens": "输入令牌",
"outputTokens": "输出令牌",
"cacheWrite": "缓存写入",
"cacheRead": "缓存读取",
"mostUsedModels": "最常用模型",
"topProjects": "热门项目",
"sessions": "会话",
"usageByModel": "按模型统计用量",
"usageByProject": "按项目统计用量",
"usageBySession": "按会话统计用量",
"timeline": "时间线",
"dailyUsage": "日常用量",
"tokens": "令牌",
"models": "模型",
"input": "输入",
"output": "输出",
"session": "会话",
"loadingUsageStats": "加载用量统计中...",
"failedToLoadUsageStats": "加载用量统计失败。请重试。",
"tryAgain": "重试",
"dailyUsageOverTime": "随时间变化的日常用量",
"noUsageData": "选定时期内无用量数据",
"cost": "成本",
"lastUsed": "上次使用",
"markdownEditorTitle": "Markdown 编辑器",
"editSystemPrompt": "编辑您的 Claude Code 系统提示",
"claudemdFile": "CLAUDE.md 文件",
"loadClaudemدFailed": "加载 CLAUDE.md 文件失败",
"saveClaudemdFailed": "保存 CLAUDE.md 文件失败",
"claudemdSavedSuccess": "CLAUDE.md 保存成功",
"saveClaudemd": "保存 CLAUDE.md",
"unsavedChangesConfirm": "您有未保存的更改。确定要离开吗?",
"servers": "服务器",
"addServer": "添加服务器",
"importExport": "导入/导出",
"mcpServerAdded": "MCP 服务器添加成功!",
"serverRemovedSuccess": "服务器 \"{{name}}\" 删除成功!",
"importedServersSuccess": "成功导入 {{count}} 个服务器!",
"importedServersFailed": "导入 {{imported}} 个服务器,{{failed}} 个失败",
"loadMcpServersFailed": "加载 MCP 服务器失败。请确保 Claude Code 已安装。",
"addMcpServer": "添加 MCP 服务器",
"configureNewMcpServer": "配置新的模型上下文协议服务器",
"serverNameRequired": "服务器名称为必填项",
"commandRequired": "命令为必填项",
"urlRequired": "URL为必填项",
"failedToAddServer": "添加服务器失败",
"environmentVariables": "环境变量",
"addVariable": "添加变量",
"serverName": "服务器名称",
"command": "命令",
"argumentsOptional": "参数(可选)",
"scope": "作用域",
"localProjectOnly": "本地(仅此项目)",
"projectSharedViaMcp": "项目(通过 .mcp.json 共享)",
"addingServer": "添加服务器中...",
"addStdioServer": "添加 Stdio 服务器",
"url": "URL",
"addSseServer": "添加 SSE 服务器",
"exampleCommands": "示例命令",
"uniqueNameToIdentify": "用于标识此服务器的唯一名称",
"commandToExecuteServer": "执行服务器的命令",
"spaceSeparatedArgs": "空格分隔的命令参数",
"sseEndpointUrl": "SSE 端点 URL",
"running": "运行中",
"showFull": "显示全部",
"hide": "隐藏",
"copied": "已复制",
"copy": "复制",
"arguments": "参数",
"environmentVariables": "环境变量",
"configuredServers": "已配置的服务器",
"serversConfigured": "个服务器已配置",
"refresh": "刷新",
"noMcpServersConfigured": "未配置 MCP 服务器",
"addServerToGetStarted": "添加一个服务器开始使用模型上下文协议",
"localProjectSpecific": "本地(项目特定)",
"projectSharedMcp": "项目(通过 .mcp.json 共享)",
"userAllProjects": "用户(所有项目)",
"letClaudeDecide": "让 Claude 决定",
"basicReasoning": "基础推理",
"deeperAnalysis": "更深入的分析",
"extensiveReasoning": "广泛推理",
"maximumAnalysis": "最大化分析",
"typeYourPromptHere": "在此输入您的提示...",
"dropImagesHere": "在此拖放图像...",
"askClaudeAnything": "向 Claude 提问任何问题...",
"usage": "用量",
"mcpServersTitle": "MCP 服务器",
"claudemdTitle": "CLAUDE.md",
"runAgent": "运行: {{name}}",
"createAgent": "创建智能体",
"importAgent": "导入智能体",
"unsavedChangesCloseConfirm": "标签页 \"{{title}}\" 有未保存的更改。仍要关闭吗?",
"welcomeToClaudia": "欢迎使用 Claudia",
"backToHome": "返回首页",
"noProjectsFound": "在 ~/.claude/projects 中未找到项目",
"failedToLoadProjects": "加载项目失败。请确保 ~/.claude 目录存在。",
"failedToLoadSessions": "加载此项目的会话失败。",
"claudeBinaryPathSaved": "Claude 二进制路径保存成功",
"noTabsOpen": "未打开标签页",
"clickPlusToStartChat": "点击 + 按钮开始新聊天",
"noAgentRunIdSpecified": "未指定智能体运行ID",
"noClaudeFileIdSpecified": "未指定Claude文件ID",
"claudeFileEditorNotImplemented": "标签页中的Claude文件编辑器尚未实现",
"noAgentDataSpecified": "未指定智能体数据",
"importAgentComingSoon": "导入智能体功能即将推出...",
"unknownTabType": "未知的标签页类型",
"selectClaudeCodeInstallation": "选择 Claude Code 安装",
"multipleInstallationsFound": "在您的系统上找到了多个 Claude Code 安装。请选择您要使用的安装。",
"claudeCodeNotFoundDialog": "在常见安装位置中未找到 Claude Code。请安装 Claude Code 以继续。",
"searchedLocations": "搜索位置",
"installationTip": "提示:您可以使用以下命令安装 Claude Code",
"searchingInstallations": "正在搜索 Claude 安装...",
"installationGuide": "安装指南",
"validating": "验证中...",
"saveSelection": "保存选择",
"noInstallationsFound": "未找到安装",
"pleaseSelectInstallation": "请选择一个 Claude 安装",
"fileSavedSuccess": "文件保存成功",
"saveFileFailed": "保存文件失败",
"loadFileFailed": "加载 CLAUDE.md 文件失败",
"editProjectSpecificPrompt": "编辑项目特定的 Claude Code 系统提示",
"maximumTabsReached": "已达到最大标签页数量 ({{max}})",
"saving": "保存中..."
},
"checkpoint": {
"title": "检查点",
@@ -121,14 +451,44 @@
"checkpointMessage": "检查点消息",
"timeline": "时间线",
"diff": "差异",
"noCheckpoints": "未找到检查点"
"deleteCheckpoint": "删除检查点",
"checkpointName": "检查点名称",
"checkpointMessage": "检查点消息",
"timeline": "时间线",
"diff": "差异",
"noCheckpoints": "未找到检查点",
"checkpointSettingsTitle": "检查点设置",
"experimentalFeature": "实验性功能",
"checkpointWarning": "检查点可能会影响目录结构或导致数据丢失。请谨慎使用。",
"automaticCheckpoints": "自动检查点",
"automaticCheckpointsDesc": "根据所选策略自动创建检查点",
"checkpointStrategy": "检查点策略",
"manualOnly": "仅手动",
"afterEachPrompt": "每个提示后",
"afterToolUse": "工具使用后",
"smart": "智能(推荐)",
"manualOnlyDesc": "检查点将仅手动创建",
"afterEachPromptDesc": "在每个用户提示后创建检查点",
"afterToolUseDesc": "在每次工具使用后创建检查点",
"smartDesc": "在破坏性操作后创建检查点",
"saving": "保存中...",
"saveSettings": "保存设置",
"storageManagement": "存储管理",
"totalCheckpoints": "总检查点数",
"keepRecentCheckpoints": "保留最近检查点",
"cleanUp": "清理",
"removeOldCheckpoints": "删除旧检查点,仅保留最近的 {{count}} 个",
"checkpointSettingsFailed": "加载检查点设置失败",
"saveCheckpointSettingsFailed": "保存检查点设置失败",
"cleanupCheckpointsFailed": "清理检查点失败",
"removedOldCheckpoints": "已删除 {{count}} 个旧检查点"
},
"placeholders": {
"searchProjects": "搜索项目...",
"searchAgents": "搜索智能体...",
"enterAgentName": "输入智能体名称...",
"enterAgentName": "例如:代码助手",
"enterSystemPrompt": "输入系统提示...",
"enterDefaultTask": "输入默认任务...",
"enterDefaultTask": "例如:检查这段代码的安全问题",
"enterURL": "输入 URL...",
"searchCommands": "搜索命令...",
"enterCommand": "输入命令...",
@@ -151,6 +511,12 @@
"unknownError": "未知错误",
"claudeCodeNotFound": "未找到 Claude Code",
"selectClaudeInstallation": "选择 Claude 安装",
"installClaudeCode": "安装 Claude Code"
"installClaudeCode": "安装 Claude Code",
"session": "会话"
},
"input": {
"pressEnterToSend": "按 Enter 发送Shift+Enter 换行",
"withFileAndCommandSupport": "@ 提及文件,/ 调用命令,拖拽或粘贴图片"
}
}