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

@@ -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>