This commit is contained in:
2025-08-07 12:28:47 +08:00
parent 6798be3b42
commit 5910362683
30 changed files with 1606 additions and 469 deletions

View File

@@ -6,6 +6,37 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Claudia - Claude Code Session Browser</title>
<!-- Maple Mono Font (Local) -->
<style>
@font-face {
font-family: 'Maple Mono';
src: url('/fonts/MapleMono-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Maple Mono';
src: url('/fonts/MapleMono-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Maple Mono';
src: url('/fonts/MapleMono-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Maple Mono';
src: url('/fonts/MapleMono-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
</style>
</head>
<body>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -13,8 +13,8 @@
"windows": [
{
"title": "Claudia",
"width": 800,
"height": 600
"width": 1600,
"height": 1200
}
],
"security": {

View File

@@ -1,12 +1,11 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Plus, Loader2, Bot, FolderCode } from "lucide-react";
import { Plus, Loader2, ArrowLeft } from "lucide-react";
import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api";
import { OutputCacheProvider } from "@/lib/outputCache";
import { TabProvider } from "@/contexts/TabContext";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ProjectList } from "@/components/ProjectList";
import { SessionList } from "@/components/SessionList";
import { RunningClaudeSessions } from "@/components/RunningClaudeSessions";
@@ -28,6 +27,7 @@ import { useTabState } from "@/hooks/useTabState";
import { AnalyticsConsentBanner } from "@/components/AnalyticsConsent";
import { useAppLifecycle, useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
import { WelcomePage } from "@/components/WelcomePage";
type View =
| "welcome"
@@ -50,7 +50,7 @@ type View =
*/
function AppContent() {
const { t } = useTranslation();
const [view, setView] = useState<View>("tabs");
const [view, setView] = useState<View>("welcome");
const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState();
const [projects, setProjects] = useState<Project[]>([]);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
@@ -149,6 +149,18 @@ function AppContent() {
};
}, []);
// Listen for switch to welcome view event
useEffect(() => {
const handleSwitchToWelcome = () => {
setView("welcome");
};
window.addEventListener('switch-to-welcome', handleSwitchToWelcome);
return () => {
window.removeEventListener('switch-to-welcome', handleSwitchToWelcome);
};
}, []);
/**
* Loads all projects from the ~/.claude/projects directory
*/
@@ -219,9 +231,9 @@ function AppContent() {
/**
* Handles view changes with navigation protection
*/
const handleViewChange = (newView: View) => {
const handleViewChange = (newView: string) => {
// No need for navigation protection with tabs since sessions stay open
setView(newView);
setView(newView as View);
};
/**
@@ -237,60 +249,10 @@ function AppContent() {
switch (view) {
case "welcome":
return (
<div className="flex items-center justify-center p-4" style={{ height: "100%" }}>
<div className="w-full max-w-4xl">
{/* Welcome Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-12 text-center"
>
<h1 className="text-4xl font-bold tracking-tight">
<span className="rotating-symbol"></span>
{t('welcomeToClaudia')}
</h1>
</motion.div>
{/* Navigation Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-2xl mx-auto">
{/* CC Agents Card */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<Card
className="h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover trailing-border"
onClick={() => handleViewChange("cc-agents")}
>
<div className="h-full flex flex-col items-center justify-center p-8">
<Bot className="h-16 w-16 mb-4 text-primary" />
<h2 className="text-xl font-semibold">{t('ccAgents')}</h2>
</div>
</Card>
</motion.div>
{/* CC Projects Card */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Card
className="h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover trailing-border"
onClick={() => handleViewChange("projects")}
>
<div className="h-full flex flex-col items-center justify-center p-8">
<FolderCode className="h-16 w-16 mb-4 text-primary" />
<h2 className="text-xl font-semibold">{t('ccProjects')}</h2>
</div>
</Card>
</motion.div>
</div>
</div>
</div>
<WelcomePage
onNavigate={handleViewChange}
onNewSession={handleNewSession}
/>
);
case "cc-agents":
@@ -325,19 +287,20 @@ function AppContent() {
transition={{ duration: 0.5 }}
className="mb-6"
>
<Button
variant="ghost"
size="sm"
onClick={() => handleViewChange("welcome")}
className="mb-4"
>
{t('backToHome')}
</Button>
<div className="mb-4">
<h1 className="text-3xl font-bold tracking-tight">{t('ccProjects')}</h1>
<p className="mt-1 text-sm text-muted-foreground">
{t('browseClaudeCodeSessions')}
</p>
<div className="flex items-center gap-3 mb-4">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewChange("welcome")}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-3xl font-bold tracking-tight">{t('ccProjects')}</h1>
<p className="mt-1 text-sm text-muted-foreground">
{t('browseClaudeCodeSessions')}
</p>
</div>
</div>
</motion.div>
@@ -375,6 +338,19 @@ function AppContent() {
projectPath={selectedProject.path}
onBack={handleBack}
onEditClaudeFile={handleEditClaudeFile}
onSessionClick={(session) => {
// Navigate to session detail view in tabs mode
setView("tabs");
// Create a new tab for this session
setTimeout(() => {
window.dispatchEvent(new CustomEvent('open-session-tab', {
detail: {
session,
projectPath: selectedProject.path
}
}));
}, 100);
}}
/>
</motion.div>
) : (
@@ -480,19 +456,19 @@ function AppContent() {
<div className="h-screen bg-background flex flex-col">
{/* Topbar */}
<Topbar
onClaudeClick={() => createClaudeMdTab()}
onSettingsClick={() => createSettingsTab()}
onUsageClick={() => createUsageTab()}
onMCPClick={() => createMCPTab()}
onClaudeClick={() => view === 'tabs' ? createClaudeMdTab() : handleViewChange('editor')}
onSettingsClick={() => view === 'tabs' ? createSettingsTab() : handleViewChange('settings')}
onUsageClick={() => view === 'tabs' ? createUsageTab() : handleViewChange('usage-dashboard')}
onMCPClick={() => view === 'tabs' ? createMCPTab() : handleViewChange('mcp')}
onInfoClick={() => setShowNFO(true)}
onAgentsClick={() => setShowAgentsModal(true)}
onAgentsClick={() => view === 'tabs' ? setShowAgentsModal(true) : handleViewChange('cc-agents')}
/>
{/* Analytics Consent Banner */}
<AnalyticsConsentBanner />
{/* Main Content */}
<div className="flex-1 overflow-hidden">
<div className="flex-1 overflow-hidden relative">
{renderContent()}
</div>

View File

@@ -230,24 +230,24 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
<div className="flex gap-2 mb-4 pt-4">
<Button onClick={handleCreateAgent} className="flex-1">
<Plus className="w-4 h-4 mr-2" />
Create Agent
{t('agents.createAgent')}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex-1">
<Import className="w-4 h-4 mr-2" />
Import Agent
{t('agents.import')}
<ChevronDown className="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleImportFromFile}>
<FileJson className="w-4 h-4 mr-2" />
From File
{t('agents.importFromFile')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleImportFromGitHub}>
<Globe className="w-4 h-4 mr-2" />
From GitHub
{t('agents.importFromGitHub')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -268,7 +268,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
window.dispatchEvent(new CustomEvent('open-create-agent-tab'));
}}>
<Plus className="w-4 h-4 mr-2" />
Create Agent
{t('agents.createAgent')}
</Button>
</div>
) : (
@@ -299,7 +299,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
onClick={() => handleExportAgent(agent)}
>
<Download className="w-3 h-3 mr-1" />
Export
{t('agents.export')}
</Button>
<Button
size="sm"
@@ -308,14 +308,14 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
className="text-destructive hover:text-destructive"
>
<Trash2 className="w-3 h-3 mr-1" />
Delete
{t('app.delete')}
</Button>
<Button
size="sm"
onClick={() => handleRunAgent(agent)}
>
<Play className="w-3 h-3 mr-1" />
Run
{t('agents.runAgent')}
</Button>
</div>
</div>
@@ -331,9 +331,9 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
{runningAgents.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<Clock className="w-12 h-12 text-muted-foreground mb-4" />
<p className="text-lg font-medium mb-2">No running agents</p>
<p className="text-lg font-medium mb-2">{t('agents.noRunningAgents')}</p>
<p className="text-sm text-muted-foreground">
Agent executions will appear here when started
{t('agents.agentExecutionsWillAppear')}
</p>
</div>
) : (
@@ -373,7 +373,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
handleOpenAgentRun(run);
}}
>
View
{t('agents.view')}
</Button>
</div>
</motion.div>
@@ -392,9 +392,9 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Agent</DialogTitle>
<DialogTitle>{t('agents.deleteAgentTitle')}</DialogTitle>
<DialogDescription>
Are you sure you want to delete "{agentToDelete?.name}"? This action cannot be undone.
{t('agents.deleteConfirmation', { name: agentToDelete?.name })}
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-3 mt-4">
@@ -405,13 +405,13 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
setAgentToDelete(null);
}}
>
Cancel
{t('app.cancel')}
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
>
Delete
{t('app.delete')}
</Button>
</div>
</DialogContent>

View File

@@ -72,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">{t('analytics.helpImproveClaudia')}</DialogTitle>
<DialogTitle className="text-2xl">{t('settings.analytics.helpImproveClaudia')}</DialogTitle>
</div>
<DialogDescription className="text-base mt-2">
{t('analytics.collectAnonymousData')}
{t('settings.analytics.collectAnonymousData')}
</DialogDescription>
</DialogHeader>
</div>
@@ -86,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">{t('analytics.whatWeCollect')}</p>
<p className="font-medium text-green-900 dark:text-green-100">{t('settings.analytics.whatWeCollect')}</p>
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
<li> {t('analytics.featureUsageDesc')}</li>
<li> {t('analytics.performanceMetricsDesc')}</li>
<li> {t('analytics.errorReportsDesc')}</li>
<li> {t('analytics.usagePatternsDesc')}</li>
<li> {t('settings.analytics.featureUsageDesc')}</li>
<li> {t('settings.analytics.performanceMetricsDesc')}</li>
<li> {t('settings.analytics.errorReportsDesc')}</li>
<li> {t('settings.analytics.usagePatternsDesc')}</li>
</ul>
</div>
</div>
@@ -101,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">{t('analytics.privacyProtected')}</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> {t('analytics.noPersonalInfo')}</li>
<li> {t('analytics.noFileContents')}</li>
<li> {t('analytics.noApiKeys')}</li>
<li> {t('analytics.anonymousData')}</li>
<li> {t('analytics.canOptOut')}</li>
<li> {t('settings.analytics.noPersonalInfo')}</li>
<li> {t('settings.analytics.noFileContents')}</li>
<li> {t('settings.analytics.noApiKeys')}</li>
<li> {t('settings.analytics.anonymousData')}</li>
<li> {t('settings.analytics.canOptOut')}</li>
</ul>
</div>
</div>
@@ -118,7 +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">
{t('analytics.dataHelpsUs')}
{t('settings.analytics.dataHelpsUs')}
</p>
</div>
</div>
@@ -130,13 +130,13 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
variant="outline"
className="flex-1"
>
{t('analytics.noThanks')}
{t('settings.analytics.noThanks')}
</Button>
<Button
onClick={handleAccept}
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
>
{t('analytics.allowAnalytics')}
{t('settings.analytics.allowAnalytics')}
</Button>
</div>
</DialogContent>
@@ -151,6 +151,7 @@ interface AnalyticsConsentBannerProps {
export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
className,
}) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [hasChecked, setHasChecked] = useState(false);
@@ -199,9 +200,9 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
<div className="flex items-start gap-3">
<BarChart3 className="h-5 w-5 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5" />
<div className="space-y-2 flex-1">
<p className="text-sm font-medium">Help improve Claudia</p>
<p className="text-sm font-medium">{t('settings.analytics.helpImproveClaudia')}</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
We collect anonymous usage data to improve your experience. No personal data is collected.
{t('settings.analytics.collectAnonymousData')}
</p>
<div className="flex gap-2 pt-1">
<Button
@@ -210,14 +211,14 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
onClick={handleDecline}
className="text-xs"
>
No Thanks
{t('settings.analytics.noThanks')}
</Button>
<Button
size="sm"
onClick={handleAccept}
className="text-xs bg-purple-600 hover:bg-purple-700 text-white"
>
Allow
{t('settings.analytics.allowAnalytics')}
</Button>
</div>
</div>

View File

@@ -134,8 +134,8 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
return (
<Card className={className}>
<CardHeader>
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.loadingAvailableInstallations')}</CardDescription>
<CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.generalOptions.loadingAvailableInstallations')}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-4">
@@ -150,8 +150,8 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
return (
<Card className={className}>
<CardHeader>
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.errorLoadingInstallations')}</CardDescription>
<CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.generalOptions.errorLoadingInstallations')}</CardDescription>
</CardHeader>
<CardContent>
<div className="text-sm text-destructive mb-4">{error}</div>
@@ -171,19 +171,19 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="h-5 w-5" />
{t('settings.claudeCodeInstallation')}
{t('settings.generalOptions.claudeCodeInstallation')}
</CardTitle>
<CardDescription>
{t('settings.choosePreferredInstallation')}
{t('settings.generalOptions.choosePreferredInstallation')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Available Installations */}
<div className="space-y-3">
<Label className="text-sm font-medium">{t('settings.availableInstallations')}</Label>
<Label className="text-sm font-medium">{t('settings.generalOptions.availableInstallations')}</Label>
<Select value={selectedInstallation?.path || ""} onValueChange={handleInstallationChange}>
<SelectTrigger>
<SelectValue placeholder="Select Claude installation">
<SelectValue placeholder={t('pleaseSelectInstallation')}>
{selectedInstallation && (
<div className="flex items-center gap-2">
{getInstallationIcon(selectedInstallation)}
@@ -198,7 +198,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
<SelectContent>
{systemInstallations.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">System Installations</div>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">{t('settings.systemInstallations')}</div>
{systemInstallations.map((installation) => (
<SelectItem key={installation.path} value={installation.path}>
<div className="flex items-center gap-2 w-full">
@@ -206,11 +206,11 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{installation.path}</div>
<div className="text-xs text-muted-foreground">
{installation.version || "Version unknown"} {installation.source}
{installation.version || t('settings.versionUnknown')} {installation.source}
</div>
</div>
<Badge variant="outline" className="text-xs">
System
{t('settings.system')}
</Badge>
</div>
</SelectItem>
@@ -220,7 +220,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
{customInstallations.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">Custom Installations</div>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">{t('settings.customInstallations')}</div>
{customInstallations.map((installation) => (
<SelectItem key={installation.path} value={installation.path}>
<div className="flex items-center gap-2 w-full">
@@ -228,11 +228,11 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{installation.path}</div>
<div className="text-xs text-muted-foreground">
{installation.version || "Version unknown"} {installation.source}
{installation.version || t('settings.versionUnknown')} {installation.source}
</div>
</div>
<Badge variant="outline" className="text-xs">
Custom
{t('settings.custom')}
</Badge>
</div>
</SelectItem>
@@ -247,16 +247,16 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
{selectedInstallation && (
<div className="p-3 bg-muted rounded-lg space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Selected Installation</span>
<span className="text-sm font-medium">{t('settings.selectedInstallation')}</span>
<Badge className={cn("text-xs", getInstallationTypeColor(selectedInstallation))}>
{selectedInstallation.installation_type}
</Badge>
</div>
<div className="text-sm text-muted-foreground">
<div><strong>Path:</strong> {selectedInstallation.path}</div>
<div><strong>Source:</strong> {selectedInstallation.source}</div>
<div><strong>{t('settings.path')}:</strong> {selectedInstallation.path}</div>
<div><strong>{t('settings.source')}:</strong> {selectedInstallation.source}</div>
{selectedInstallation.version && (
<div><strong>Version:</strong> {selectedInstallation.version}</div>
<div><strong>{t('settings.version')}:</strong> {selectedInstallation.version}</div>
)}
</div>
</div>
@@ -269,7 +269,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
disabled={isSaving || !selectedInstallation}
className="w-full"
>
{isSaving ? "Saving..." : "Save Selection"}
{isSaving ? t('saving') : t('saveSelection')}
</Button>
)}
</CardContent>

View File

@@ -0,0 +1,198 @@
import { motion } from "framer-motion";
interface ClaudiaLogoProps {
size?: number;
className?: string;
}
export function ClaudiaLogo({ size = 48, className = "" }: ClaudiaLogoProps) {
return (
<motion.div
className={`relative inline-flex items-center justify-center ${className}`}
style={{ width: size, height: size }}
>
{/* Background glow animation */}
<motion.div
className="absolute inset-0 rounded-2xl"
style={{
background: "radial-gradient(circle, rgba(251, 146, 60, 0.3) 0%, transparent 70%)",
}}
animate={{
scale: [1, 1.3, 1],
opacity: [0.5, 0.8, 0.5],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Main logo with image */}
<motion.div
className="relative w-full h-full rounded-2xl overflow-hidden shadow-xl"
animate={{
rotateY: [0, 360],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: "linear",
}}
whileHover={{
scale: 1.1,
transition: {
duration: 0.3,
ease: "easeOut",
},
}}
>
{/* Use the actual Claudia icon */}
<motion.img
src="/icon.png"
alt="Claudia"
className="w-full h-full object-contain"
animate={{
scale: [1, 1.05, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Shimmer effect overlay */}
<motion.div
className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent"
animate={{
x: ["-200%", "200%"],
}}
transition={{
duration: 3,
repeat: Infinity,
repeatDelay: 1,
ease: "easeInOut",
}}
/>
</motion.div>
{/* Orbiting particles */}
{[...Array(4)].map((_, i) => (
<motion.div
key={i}
className="absolute w-1.5 h-1.5 bg-orange-400 rounded-full"
style={{
top: "50%",
left: "50%",
}}
animate={{
x: [
0,
Math.cos((i * Math.PI) / 2) * size * 0.6,
0,
-Math.cos((i * Math.PI) / 2) * size * 0.6,
0,
],
y: [
0,
Math.sin((i * Math.PI) / 2) * size * 0.6,
0,
-Math.sin((i * Math.PI) / 2) * size * 0.6,
0,
],
}}
transition={{
duration: 4,
repeat: Infinity,
delay: i * 0.25,
ease: "easeInOut",
}}
/>
))}
</motion.div>
);
}
// Alternative minimalist version
export function ClaudiaLogoMinimal({ size = 48, className = "" }: ClaudiaLogoProps) {
return (
<motion.div
className={`relative inline-flex items-center justify-center ${className}`}
style={{ width: size, height: size }}
animate={{
rotate: [0, 5, -5, 5, 0],
}}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut",
}}
>
{/* Gradient background */}
<motion.div
className="absolute inset-0 rounded-2xl bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600"
animate={{
scale: [1, 1.1, 1],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Inner light effect */}
<motion.div
className="absolute inset-1 rounded-xl bg-gradient-to-br from-orange-300/50 to-transparent"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Letter C with animation */}
<motion.div
className="relative z-10 text-white font-bold flex items-center justify-center"
style={{ fontSize: size * 0.5 }}
animate={{
scale: [1, 1.1, 1],
textShadow: [
"0 0 10px rgba(255,255,255,0.5)",
"0 0 20px rgba(255,255,255,0.8)",
"0 0 10px rgba(255,255,255,0.5)",
],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
C
</motion.div>
{/* Pulse rings */}
{[...Array(2)].map((_, i) => (
<motion.div
key={i}
className="absolute inset-0 rounded-2xl border-2 border-orange-400"
animate={{
scale: [1, 1.5, 2],
opacity: [0.5, 0.2, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
delay: i * 1.5,
ease: "easeOut",
}}
/>
))}
</motion.div>
);
}

View File

@@ -81,33 +81,33 @@ interface EditableHookMatcher extends Omit<HookMatcher, 'hooks'> {
expanded?: boolean;
}
const EVENT_INFO: Record<HookEvent, { label: string; description: string; icon: React.ReactNode }> = {
const getEventInfo = (t: any): Record<HookEvent, { label: string; description: string; icon: React.ReactNode }> => ({
PreToolUse: {
label: 'Pre Tool Use',
description: 'Runs before tool calls, can block and provide feedback',
label: t('hooks.preToolUse'),
description: t('hooks.runsBeforeToolCalls'),
icon: <Shield className="h-4 w-4" />
},
PostToolUse: {
label: 'Post Tool Use',
description: 'Runs after successful tool completion',
label: t('hooks.postToolUse'),
description: t('hooks.runsAfterToolCompletion'),
icon: <PlayCircle className="h-4 w-4" />
},
Notification: {
label: 'Notification',
description: 'Customizes notifications when Claude needs attention',
label: t('hooks.notification'),
description: t('hooks.customizesNotifications'),
icon: <Zap className="h-4 w-4" />
},
Stop: {
label: 'Stop',
description: 'Runs when Claude finishes responding',
label: t('hooks.stop'),
description: t('hooks.runsWhenClaudeFinishes'),
icon: <Code2 className="h-4 w-4" />
},
SubagentStop: {
label: 'Subagent Stop',
description: 'Runs when a Claude subagent (Task) finishes',
label: t('hooks.subagentStop'),
description: t('hooks.runsWhenSubagentFinishes'),
icon: <Terminal className="h-4 w-4" />
}
};
});
export const HooksEditor: React.FC<HooksEditorProps> = ({
projectPath,
@@ -118,6 +118,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
hideActions = false
}) => {
const { t } = useTranslation();
const EVENT_INFO = getEventInfo(t);
const [selectedEvent, setSelectedEvent] = useState<HookEvent>('PreToolUse');
const [showTemplateDialog, setShowTemplateDialog] = useState(false);
const [validationErrors, setValidationErrors] = useState<string[]>([]);
@@ -525,14 +526,14 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor={`matcher-${matcher.id}`}>Pattern</Label>
<Label htmlFor={`matcher-${matcher.id}`}>{t('hooks.pattern')}</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Tool name pattern (regex supported). Leave empty to match all tools.</p>
<p>{t('hooks.toolNamePatternTooltip')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -558,7 +559,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
disabled={readOnly}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Common patterns" />
<SelectValue placeholder={t('hooks.commonPatterns')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="custom">Custom</SelectItem>
@@ -591,7 +592,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Commands</Label>
<Label>{t('hooks.commands')}</Label>
{!readOnly && (
<Button
variant="outline"
@@ -599,7 +600,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
onClick={() => addCommand(event, matcher.id)}
>
<Plus className="h-3 w-3 mr-1" />
Add Command
{t('hooks.addCommand')}
</Button>
)}
</div>
@@ -613,7 +614,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<div className="flex items-start gap-2">
<div className="flex-1 space-y-2">
<Textarea
placeholder="Enter shell command..."
placeholder={t('hooks.enterShellCommand')}
value={hook.command || ''}
onChange={(e) => updateCommand(event, matcher.id, hook.id, { command: e.target.value })}
disabled={readOnly}
@@ -633,7 +634,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
disabled={readOnly}
className="w-20 h-8"
/>
<span className="text-sm text-muted-foreground">seconds</span>
<span className="text-sm text-muted-foreground">{t('hooks.seconds')}</span>
</div>
{!readOnly && (
@@ -679,7 +680,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<div className="flex items-start gap-2">
<div className="flex-1 space-y-2">
<Textarea
placeholder="Enter shell command..."
placeholder={t('hooks.enterShellCommand')}
value={command.command || ''}
onChange={(e) => updateDirectCommand(event, command.id, { command: e.target.value })}
disabled={readOnly}
@@ -738,7 +739,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{isLoading && (
<div className="flex items-center justify-center p-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">Loading hooks configuration...</span>
<span className="text-sm text-muted-foreground">{t('hooks.loadingHooksConfiguration')}</span>
</div>
)}
@@ -759,7 +760,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<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
{scope === 'project' ? t('hooks.projectScope') : scope === 'local' ? t('hooks.localScope') : t('hooks.userScope')}
</Badge>
{!readOnly && (
<>
@@ -769,7 +770,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
onClick={() => setShowTemplateDialog(true)}
>
<FileText className="h-4 w-4 mr-2" />
Templates
{t('hooks.templates')}
</Button>
{!hideActions && (
<Button
@@ -783,7 +784,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
) : (
<Save className="h-4 w-4 mr-2" />
)}
{isSaving ? "Saving..." : "Save"}
{isSaving ? t('hooks.saving') : t('hooks.save')}
</Button>
)}
</>
@@ -804,7 +805,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{/* Validation Messages */}
{validationErrors.length > 0 && (
<div className="p-3 bg-red-500/10 rounded-md space-y-1">
<p className="text-sm font-medium text-red-600">Validation Errors:</p>
<p className="text-sm font-medium text-red-600">{t('hooks.validationErrors')}:</p>
{validationErrors.map((error, i) => (
<p key={i} className="text-xs text-red-600"> {error}</p>
))}
@@ -813,7 +814,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{validationWarnings.length > 0 && (
<div className="p-3 bg-yellow-500/10 rounded-md space-y-1">
<p className="text-sm font-medium text-yellow-600">Security Warnings:</p>
<p className="text-sm font-medium text-yellow-600">{t('hooks.securityWarnings')}:</p>
{validationWarnings.map((warning, i) => (
<p key={i} className="text-xs text-yellow-600"> {warning}</p>
))}
@@ -859,11 +860,11 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{items.length === 0 ? (
<Card className="p-8 text-center">
<p className="text-muted-foreground mb-4">No hooks configured for this event</p>
<p className="text-muted-foreground mb-4">{t('hooks.noHooksConfigured')}</p>
{!readOnly && (
<Button onClick={() => isMatcherEvent ? addMatcher(event) : addDirectCommand(event)}>
<Plus className="h-4 w-4 mr-2" />
Add Hook
{t('hooks.addHook')}
</Button>
)}
</Card>
@@ -881,7 +882,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
Add Another {isMatcherEvent ? 'Matcher' : 'Command'}
{isMatcherEvent ? t('hooks.addAnotherMatcher') : t('hooks.addAnotherCommand')}
</Button>
)}
</div>
@@ -895,9 +896,9 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Hook Templates</DialogTitle>
<DialogTitle>{t('hooks.hookTemplates')}</DialogTitle>
<DialogDescription>
Choose a pre-configured hook template to get started quickly
{t('hooks.quickStartTemplates')}
</DialogDescription>
</DialogHeader>
@@ -916,7 +917,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<p className="text-sm text-muted-foreground">{template.description}</p>
{matcherEvents.includes(template.event as any) && template.matcher && (
<p className="text-xs font-mono bg-muted px-2 py-1 rounded inline-block">
Matcher: {template.matcher}
{t('hooks.pattern')}: {template.matcher}
</p>
)}
</div>

View File

@@ -145,6 +145,7 @@ import {
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
/**
* Icon categories for better organization
@@ -333,6 +334,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
}) => {
const [searchQuery, setSearchQuery] = useState("");
const [hoveredIcon, setHoveredIcon] = useState<string | null>(null);
const { t } = useTranslation();
// Filter icons based on search query
const filteredCategories = useMemo(() => {
@@ -368,7 +370,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[80vh] p-0">
<DialogHeader className="px-6 py-4 border-b">
<DialogTitle>Choose an icon</DialogTitle>
<DialogTitle>{t('agents.chooseAnIcon')}</DialogTitle>
</DialogHeader>
{/* Search Bar */}
@@ -376,7 +378,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search icons..."
placeholder={t('agents.searchIcons')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
@@ -390,7 +392,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
{Object.keys(filteredCategories).length === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-center">
<p className="text-sm text-muted-foreground">
No icons found for "{searchQuery}"
{t('agents.noIconsFound')} "{searchQuery}"
</p>
</div>
) : (
@@ -405,7 +407,15 @@ export const IconPicker: React.FC<IconPickerProps> = ({
transition={{ duration: 0.2 }}
>
<h3 className="text-sm font-medium text-muted-foreground mb-3">
{category}
{category === "Interface & Navigation" ? t('agents.iconCategories.interfaceNavigation') :
category === "Development & Tech" ? t('agents.iconCategories.developmentTech') :
category === "Business & Finance" ? t('agents.iconCategories.businessFinance') :
category === "Creative & Design" ? t('agents.iconCategories.creativeDesign') :
category === "Nature & Science" ? t('agents.iconCategories.natureScience') :
category === "Gaming & Entertainment" ? t('agents.iconCategories.gamingEntertainment') :
category === "Communication" ? t('agents.iconCategories.communication') :
category === "Miscellaneous" ? t('agents.iconCategories.miscellaneous') :
category}
</h3>
<div className="grid grid-cols-10 gap-2">
{icons.map((item: IconItem) => {
@@ -444,7 +454,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
{/* Footer */}
<div className="px-6 py-3 border-t bg-muted/50">
<p className="text-xs text-muted-foreground text-center">
Click an icon to select {allIcons.length} icons available
{t('agents.clickToSelect')} {allIcons.length} {t('agents.iconsAvailable')}
</p>
</div>
</DialogContent>

View File

@@ -108,7 +108,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
}
if (!stdioCommand.trim()) {
onError(t('commandRequired'));
onError(t('mcp.commandRequired'));
return;
}
@@ -154,7 +154,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onError(result.message);
}
} catch (error) {
onError(t('failedToAddServer'));
onError(t('mcp.failedToAddServer'));
console.error("Failed to add stdio server:", error);
} finally {
setSaving(false);
@@ -171,7 +171,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
}
if (!sseUrl.trim()) {
onError(t('urlRequired'));
onError(t('mcp.urlRequired'));
return;
}
@@ -213,7 +213,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onError(result.message);
}
} catch (error) {
onError(t('failedToAddServer'));
onError(t('mcp.failedToAddServer'));
console.error("Failed to add SSE server:", error);
} finally {
setSaving(false);
@@ -227,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">{t('environmentVariables')}</Label>
<Label className="text-sm font-medium">{t('mcp.environmentVariables')}</Label>
<Button
variant="outline"
size="sm"
@@ -235,7 +235,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="gap-2"
>
<Plus className="h-3 w-3" />
{t('addVariable')}
{t('mcp.addVariable')}
</Button>
</div>
@@ -244,14 +244,14 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{envVars.map((envVar) => (
<div key={envVar.id} className="flex items-center gap-2">
<Input
placeholder="KEY"
placeholder={t('settings.placeholders.envVarKey')}
value={envVar.key}
onChange={(e) => updateEnvVar(type, 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(type, envVar.id, "value", e.target.value)}
className="flex-1 font-mono text-sm"
@@ -275,9 +275,9 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
return (
<div className="p-6 space-y-6">
<div>
<h3 className="text-base font-semibold">{t('addMcpServer')}</h3>
<h3 className="text-base font-semibold">{t('mcp.addMcpServer')}</h3>
<p className="text-sm text-muted-foreground mt-1">
{t('configureNewMcpServer')}
{t('mcp.configureNewMcpServer')}
</p>
</div>
@@ -298,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">{t('serverName')}</Label>
<Label htmlFor="stdio-name">{t('mcp.serverName')}</Label>
<Input
id="stdio-name"
placeholder="my-server"
@@ -306,12 +306,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onChange={(e) => setStdioName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
{t('uniqueNameToIdentify')}
{t('mcp.uniqueNameToIdentify')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="stdio-command">{t('command')}</Label>
<Label htmlFor="stdio-command">{t('mcp.command')}</Label>
<Input
id="stdio-command"
placeholder="/path/to/server"
@@ -320,12 +320,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
{t('commandToExecuteServer')}
{t('mcp.commandToExecuteServer')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="stdio-args">{t('argumentsOptional')}</Label>
<Label htmlFor="stdio-args">{t('mcp.argumentsOptional')}</Label>
<Input
id="stdio-args"
placeholder="arg1 arg2 arg3"
@@ -334,19 +334,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
{t('spaceSeparatedArgs')}
{t('mcp.spaceSeparatedArgs')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="stdio-scope">{t('scope')}</Label>
<Label htmlFor="stdio-scope">{t('mcp.scope')}</Label>
<SelectComponent
value={stdioScope}
onValueChange={(value: string) => setStdioScope(value)}
options={[
{ value: "local", label: t('localProjectOnly') },
{ value: "project", label: t('projectSharedViaMcp') },
{ value: "user", label: t('userAllProjects') },
{ value: "local", label: t('mcp.localProjectOnly') },
{ value: "project", label: t('mcp.projectSharedViaMcp') },
{ value: "user", label: t('mcp.userAllProjects') },
]}
/>
</div>
@@ -363,12 +363,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{t('addingServer')}
{t('mcp.addingServer')}
</>
) : (
<>
<Plus className="h-4 w-4" />
{t('addStdioServer')}
{t('mcp.addStdioServer')}
</>
)}
</Button>
@@ -381,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">{t('serverName')}</Label>
<Label htmlFor="sse-name">{t('mcp.serverName')}</Label>
<Input
id="sse-name"
placeholder="sse-server"
@@ -389,12 +389,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onChange={(e) => setSseName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
{t('uniqueNameToIdentify')}
{t('mcp.uniqueNameToIdentify')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="sse-url">{t('url')}</Label>
<Label htmlFor="sse-url">{t('mcp.url')}</Label>
<Input
id="sse-url"
placeholder="https://example.com/sse-endpoint"
@@ -403,19 +403,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
{t('sseEndpointUrl')}
{t('mcp.sseEndpointUrl')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="sse-scope">{t('scope')}</Label>
<Label htmlFor="sse-scope">{t('mcp.scope')}</Label>
<SelectComponent
value={sseScope}
onValueChange={(value: string) => setSseScope(value)}
options={[
{ value: "local", label: t('localProjectOnly') },
{ value: "project", label: t('projectSharedViaMcp') },
{ value: "user", label: t('userAllProjects') },
{ value: "local", label: t('mcp.localProjectOnly') },
{ value: "project", label: t('mcp.projectSharedViaMcp') },
{ value: "user", label: t('mcp.userAllProjects') },
]}
/>
</div>
@@ -432,12 +432,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{t('addingServer')}
{t('mcp.addingServer')}
</>
) : (
<>
<Plus className="h-4 w-4" />
{t('addSseServer')}
{t('mcp.addSseServer')}
</>
)}
</Button>
@@ -451,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>{t('exampleCommands')}</span>
<span>{t('mcp.exampleCommands')}</span>
</div>
<div className="space-y-2 text-xs text-muted-foreground">
<div className="font-mono bg-background p-2 rounded">

View File

@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { SelectComponent } from "@/components/ui/select";
import { api } from "@/lib/api";
import { useTranslation } from "@/hooks/useTranslation";
interface MCPImportExportProps {
/**
@@ -24,6 +25,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
onImportCompleted,
onError,
}) => {
const { t } = useTranslation();
const [importingDesktop, setImportingDesktop] = useState(false);
const [importingJson, setImportingJson] = useState(false);
const [importScope, setImportScope] = useState("local");
@@ -163,9 +165,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
return (
<div className="p-6 space-y-6">
<div>
<h3 className="text-base font-semibold">Import & Export</h3>
<h3 className="text-base font-semibold">{t('mcp.importExport')}</h3>
<p className="text-sm text-muted-foreground mt-1">
Import MCP servers from other sources or export your configuration
{t('mcp.importExportDescription')}
</p>
</div>
@@ -175,19 +177,19 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<div className="space-y-3">
<div className="flex items-center gap-2 mb-2">
<Settings2 className="h-4 w-4 text-slate-500" />
<Label className="text-sm font-medium">Import Scope</Label>
<Label className="text-sm font-medium">{t('mcp.importScope')}</Label>
</div>
<SelectComponent
value={importScope}
onValueChange={(value: string) => setImportScope(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('mcp.localProjectOnly') },
{ value: "project", label: t('mcp.projectShared') },
{ value: "user", label: t('mcp.userAllProjects') },
]}
/>
<p className="text-xs text-muted-foreground">
Choose where to save imported servers from JSON files
{t('mcp.chooseImportLocation')}
</p>
</div>
</Card>
@@ -200,9 +202,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<Download className="h-5 w-5 text-blue-500" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Import from Claude Desktop</h4>
<h4 className="text-sm font-medium">{t('mcp.importFromClaudeDesktop')}</h4>
<p className="text-xs text-muted-foreground mt-1">
Automatically imports all MCP servers from Claude Desktop. Installs to user scope (available across all projects).
{t('mcp.importFromClaudeDesktopDescription')}
</p>
</div>
</div>
@@ -214,12 +216,12 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
{importingDesktop ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Importing...
{t('mcp.importing')}
</>
) : (
<>
<Download className="h-4 w-4" />
Import from Claude Desktop
{t('mcp.importFromClaudeDesktop')}
</>
)}
</Button>
@@ -234,9 +236,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<FileText className="h-5 w-5 text-purple-500" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Import from JSON</h4>
<h4 className="text-sm font-medium">{t('mcp.importFromJSON')}</h4>
<p className="text-xs text-muted-foreground mt-1">
Import server configuration from a JSON file
{t('mcp.importFromJSONDescription')}
</p>
</div>
</div>
@@ -258,12 +260,12 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
{importingJson ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Importing...
{t('mcp.importing')}
</>
) : (
<>
<FileText className="h-4 w-4" />
Choose JSON File
{t('mcp.chooseJSONFile')}
</>
)}
</Button>
@@ -279,9 +281,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<Upload className="h-5 w-5 text-muted-foreground" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Export Configuration</h4>
<h4 className="text-sm font-medium">{t('mcp.exportConfiguration')}</h4>
<p className="text-xs text-muted-foreground mt-1">
Export your MCP server configuration
{t('mcp.exportConfigurationDescription')}
</p>
</div>
</div>
@@ -292,7 +294,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
className="w-full gap-2"
>
<Upload className="h-4 w-4" />
Export (Coming Soon)
{t('mcp.exportComingSoon')}
</Button>
</div>
</Card>
@@ -305,9 +307,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<Network className="h-5 w-5 text-green-500" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Use Claude Code as MCP Server</h4>
<h4 className="text-sm font-medium">{t('mcp.useClaudeCodeAsMCPServer')}</h4>
<p className="text-xs text-muted-foreground mt-1">
Start Claude Code as an MCP server that other applications can connect to
{t('mcp.useClaudeCodeAsMCPServerDescription')}
</p>
</div>
</div>
@@ -317,7 +319,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
className="w-full gap-2 border-green-500/20 hover:bg-green-500/10 hover:text-green-600 hover:border-green-500/50"
>
<Network className="h-4 w-4" />
Start MCP Server
{t('mcp.startMCPServer')}
</Button>
</div>
</Card>
@@ -328,11 +330,11 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<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>JSON Format Examples</span>
<span>{t('mcp.jsonFormatExamples')}</span>
</div>
<div className="space-y-3 text-xs">
<div>
<p className="font-medium text-muted-foreground mb-1">Single server:</p>
<p className="font-medium text-muted-foreground mb-1">{t('mcp.singleServer')}:</p>
<pre className="bg-background p-3 rounded-lg overflow-x-auto">
{`{
"type": "stdio",
@@ -343,7 +345,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
</pre>
</div>
<div>
<p className="font-medium text-muted-foreground mb-1">Multiple servers (.mcp.json format):</p>
<p className="font-medium text-muted-foreground mb-1">{t('mcp.multipleServers')}:</p>
<pre className="bg-background p-3 rounded-lg overflow-x-auto">
{`{
"mcpServers": {

View File

@@ -155,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" />
{t('servers')}
{t('mcp.servers')}
</TabsTrigger>
<TabsTrigger value="add" className="gap-2">
<Plus className="h-4 w-4 text-green-500" />
{t('addServer')}
{t('mcp.addServer')}
</TabsTrigger>
<TabsTrigger value="import" className="gap-2">
<Download className="h-4 w-4 text-purple-500" />
{t('importExport')}
{t('mcp.importExport')}
</TabsTrigger>
</TabsList>

View File

@@ -186,11 +186,11 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
const getScopeDisplayName = (scope: string) => {
switch (scope) {
case "local":
return t('localProjectSpecific');
return t('mcp.localProjectSpecific');
case "project":
return t('projectSharedMcp');
return t('mcp.projectSharedMcp');
case "user":
return t('userAllProjects');
return t('mcp.userAllProjects');
default:
return scope;
}
@@ -222,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" />
{t('running')}
{t('mcp.running')}
</Badge>
)}
</div>
@@ -239,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" />
{t('showFull')}
{t('mcp.showFull')}
</Button>
</div>
)}
@@ -254,7 +254,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{Object.keys(server.env).length > 0 && !isExpanded && (
<div className="flex items-center gap-1 text-xs text-muted-foreground pl-9">
<span>Environment variables: {Object.keys(server.env).length}</span>
<span>{t('mcp.environmentVariablesCount', { count: Object.keys(server.env).length })}</span>
</div>
)}
</div>
@@ -301,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">{t('command')}</p>
<p className="text-xs font-medium text-muted-foreground">{t('mcp.command')}</p>
<div className="flex items-center gap-1">
<Button
variant="ghost"
@@ -310,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 ? t('copied') : t('copy')}
{isCopied ? t('mcp.copied') : t('mcp.copy')}
</Button>
<Button
variant="ghost"
@@ -319,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" />
{t('hide')}
{t('mcp.hide')}
</Button>
</div>
</div>
@@ -331,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">{t('arguments')}</p>
<p className="text-xs font-medium text-muted-foreground">{t('mcp.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">
@@ -345,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">{t('url')}</p>
<p className="text-xs font-medium text-muted-foreground">{t('mcp.url')}</p>
<p className="text-xs font-mono bg-muted/50 p-2 rounded break-all">
{server.url}
</p>
@@ -354,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">{t('environmentVariables')}</p>
<p className="text-xs font-medium text-muted-foreground">{t('mcp.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">
@@ -386,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">{t('configuredServers')}</h3>
<h3 className="text-base font-semibold">{t('mcp.configuredServers')}</h3>
<p className="text-sm text-muted-foreground">
{servers.length} {servers.length !== 1 ? t('servers') : 'server'} {t('serversConfigured')}
{servers.length} {t('mcp.serversCount', { count: servers.length })}
</p>
</div>
<Button

View File

@@ -53,7 +53,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
setOriginalContent(prompt);
} catch (err) {
console.error("Failed to load system prompt:", err);
setError(t('loadClaudemdFailed'));
setError(t('usage.loadClaudemdFailed'));
} finally {
setLoading(false);
}
@@ -66,11 +66,11 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
setToast(null);
await api.saveSystemPrompt(content);
setOriginalContent(content);
setToast({ message: t('claudemdSavedSuccess'), type: "success" });
setToast({ message: t('usage.claudemdSavedSuccess'), type: "success" });
} catch (err) {
console.error("Failed to save system prompt:", err);
setError(t('saveClaudemdFailed'));
setToast({ message: t('saveClaudemdFailed'), type: "error" });
setError(t('usage.saveClaudemdFailed'));
setToast({ message: t('usage.saveClaudemdFailed'), type: "error" });
} finally {
setSaving(false);
}
@@ -79,7 +79,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
const handleBack = () => {
if (hasChanges) {
const confirmLeave = window.confirm(
t('unsavedChangesConfirm')
t('usage.unsavedChangesConfirm')
);
if (!confirmLeave) return;
}
@@ -108,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">
{t('editSystemPrompt')}
{t('usage.editSystemPrompt')}
</p>
</div>
</div>

View File

@@ -85,8 +85,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={onBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" />

View File

@@ -315,6 +315,22 @@ 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();
const [hasInitialized, setHasInitialized] = React.useState(false);
// Auto redirect to home when no tabs (but not on initial load)
useEffect(() => {
if (hasInitialized && tabs.length === 0) {
// Dispatch event to switch back to welcome view
setTimeout(() => {
window.dispatchEvent(new CustomEvent('switch-to-welcome'));
}, 100);
}
}, [tabs.length, hasInitialized]);
// Mark as initialized after first render
useEffect(() => {
setHasInitialized(true);
}, []);
// Listen for events to open sessions in tabs
useEffect(() => {

View File

@@ -142,6 +142,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
createProjectsTab,
closeTab,
switchToTab,
updateTab,
canAddTab
} = useTabState();
@@ -209,11 +210,30 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
}
};
const handleOpenSessionTab = (event: CustomEvent) => {
const { session, projectPath } = event.detail;
if (session && canAddTab()) {
// Create a new chat tab with the session data
const tabId = createChatTab();
// Update the tab with session data
setTimeout(() => {
updateTab(tabId, {
type: 'chat',
title: session.project_path.split('/').pop() || 'Session',
sessionId: session.id,
sessionData: session,
initialProjectPath: projectPath || session.project_path,
});
}, 100);
}
};
window.addEventListener('create-chat-tab', handleCreateTab);
window.addEventListener('close-current-tab', handleCloseTab);
window.addEventListener('switch-to-next-tab', handleNextTab);
window.addEventListener('switch-to-previous-tab', handlePreviousTab);
window.addEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);
window.addEventListener('open-session-tab', handleOpenSessionTab as EventListener);
return () => {
window.removeEventListener('create-chat-tab', handleCreateTab);
@@ -221,8 +241,9 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
window.removeEventListener('switch-to-next-tab', handleNextTab);
window.removeEventListener('switch-to-previous-tab', handlePreviousTab);
window.removeEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);
window.removeEventListener('open-session-tab', handleOpenSessionTab as EventListener);
};
}, [tabs, activeTabId, createChatTab, closeTab, switchToTab]);
}, [tabs, activeTabId, createChatTab, closeTab, switchToTab, updateTab, canAddTab]);
// Check scroll buttons visibility
const checkScrollButtons = () => {
@@ -319,7 +340,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
className={cn(
"p-1.5 hover:bg-muted/80 rounded-sm z-20 ml-1",
"transition-colors duration-200 flex items-center justify-center",
"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50"
"bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60"
)}
title="Scroll tabs left"
>
@@ -373,7 +394,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
className={cn(
"p-1.5 hover:bg-muted/80 rounded-sm z-20 mr-1",
"transition-colors duration-200 flex items-center justify-center",
"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50"
"bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60"
)}
title="Scroll tabs right"
>
@@ -390,7 +411,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
disabled={!canAddTab()}
className={cn(
"p-2 mx-2 rounded-md transition-all duration-200 flex items-center justify-center",
"border border-border/50 bg-background/50 backdrop-blur-sm",
"border border-border/60 bg-background/85 backdrop-blur-xl backdrop-saturate-[1.8]",
canAddTab()
? "hover:bg-muted/80 hover:border-border text-muted-foreground hover:text-foreground hover:shadow-sm"
: "opacity-50 cursor-not-allowed bg-muted/30"

View File

@@ -107,7 +107,11 @@ export const Topbar: React.FC<TopbarProps> = ({
variant="ghost"
size="sm"
className="h-auto py-1 px-2 hover:bg-accent"
onClick={onSettingsClick}
onClick={() => {
// Emit event to return to home
window.dispatchEvent(new CustomEvent('switch-to-welcome'));
}}
title="Return to Home"
>
<div className="flex items-center space-x-2 text-xs">
<Circle
@@ -172,7 +176,7 @@ export const Topbar: React.FC<TopbarProps> = ({
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={cn(
"flex items-center justify-between px-4 py-3 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
"flex items-center justify-between px-4 py-3 border-b border-border bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] supports-[backdrop-filter]:bg-background/85 shadow-sm",
className
)}
>

View File

@@ -16,6 +16,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
interface WebviewPreviewProps {
/**
@@ -61,6 +62,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
onUrlChange,
className,
}) => {
const { t } = useTranslation();
const [currentUrl, setCurrentUrl] = useState(initialUrl);
const [inputUrl, setInputUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
@@ -132,7 +134,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
onUrlChange?.(finalUrl);
} catch (err) {
setHasError(true);
setErrorMessage("Invalid URL");
setErrorMessage(t('webview.invalidUrl'));
}
};
@@ -182,7 +184,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
<div className="flex items-center justify-between px-3 py-2 border-b">
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Preview</span>
<span className="text-sm font-medium">{t('webview.preview')}</span>
{isLoading && (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
)}
@@ -207,7 +209,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
</Button>
</TooltipTrigger>
<TooltipContent>
{isMaximized ? "Exit full screen (ESC)" : "Enter full screen"}
{isMaximized ? t('webview.exitFullScreen') : t('webview.enterFullScreen')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -270,7 +272,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter URL..."
placeholder={t('webview.enterUrl')}
className="pr-10 h-8 text-sm font-mono"
/>
{inputUrl !== currentUrl && (
@@ -300,7 +302,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
>
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">Loading preview...</p>
<p className="text-sm text-muted-foreground">{t('webview.loadingPreview')}</p>
</div>
</motion.div>
)}
@@ -310,12 +312,12 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
{hasError ? (
<div className="flex flex-col items-center justify-center h-full p-8">
<AlertCircle className="h-12 w-12 text-destructive mb-4" />
<h3 className="text-lg font-semibold mb-2">Failed to load preview</h3>
<h3 className="text-lg font-semibold mb-2">{t('webview.failedToLoad')}</h3>
<p className="text-sm text-muted-foreground text-center mb-4">
{errorMessage || "The page could not be loaded. Please check the URL and try again."}
{errorMessage || t('webview.pageCouldNotLoad')}
</p>
<Button onClick={handleRefresh} variant="outline" size="sm">
Try Again
{t('app.retry')}
</Button>
</div>
) : currentUrl ? (
@@ -336,14 +338,14 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
// Empty state when no URL is provided
<div className="flex flex-col items-center justify-center h-full p-8 text-foreground">
<Globe className="h-16 w-16 text-muted-foreground/50 mb-6" />
<h3 className="text-xl font-semibold mb-3">Enter a URL to preview</h3>
<h3 className="text-xl font-semibold mb-3">{t('webview.enterUrlToPreview')}</h3>
<p className="text-sm text-muted-foreground text-center mb-6 max-w-md">
Enter a URL in the address bar above to preview a website.
{t('webview.enterUrlDescription')}
</p>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>Try entering</span>
<span>{t('webview.tryEntering')}</span>
<code className="px-2 py-1 bg-muted/50 text-foreground rounded font-mono text-xs">localhost:3000</code>
<span>or any other URL</span>
<span>{t('webview.orAnyOtherUrl')}</span>
</div>
</div>
)}

View File

@@ -0,0 +1,207 @@
import { motion } from "framer-motion";
import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings } from "lucide-react";
import { useTranslation } from "@/hooks/useTranslation";
import { Button } from "@/components/ui/button";
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
import { BorderGlowCard } from "@/components/ui/glow-card";
interface WelcomePageProps {
onNavigate: (view: string) => void;
onNewSession: () => void;
}
export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
const { t } = useTranslation();
const mainFeatures = [
{
id: "agents",
icon: Bot,
title: t("welcome.agentManagement"),
subtitle: t("welcome.agentManagementDesc"),
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "cc-agents"
},
{
id: "projects",
icon: FolderCode,
title: t("welcome.projectManagement"),
subtitle: t("welcome.projectManagementDesc"),
color: "text-blue-500",
bgColor: "bg-blue-500/10",
view: "projects"
}
];
const bottomFeatures = [
{
id: "usage",
icon: BarChart,
title: t("welcome.usageStatistics"),
subtitle: t("welcome.usageStatisticsDesc"),
color: "text-green-500",
bgColor: "bg-green-500/10",
view: "usage-dashboard"
},
{
id: "mcp",
icon: ServerCog,
title: t("welcome.mcpBroker"),
subtitle: t("welcome.mcpBrokerDesc"),
color: "text-purple-500",
bgColor: "bg-purple-500/10",
view: "mcp"
},
{
id: "claude-md",
icon: FileText,
title: t("welcome.claudeMd"),
subtitle: t("welcome.claudeMdDesc"),
color: "text-cyan-500",
bgColor: "bg-cyan-500/10",
view: "editor"
},
{
id: "settings",
icon: Settings,
title: t("welcome.settings"),
subtitle: t("welcome.settingsDesc"),
color: "text-gray-500",
bgColor: "bg-gray-500/10",
view: "settings"
}
];
const handleCardClick = (view: string) => {
onNavigate(view);
};
const handleButtonClick = () => {
onNewSession();
};
return (
<div className="flex items-center justify-center min-h-screen bg-background overflow-hidden">
<div className="w-full max-w-6xl px-8">
{/* Header */}
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h1 className="text-5xl font-bold mb-4 flex items-center justify-center gap-4 bg-gradient-to-r from-orange-400 via-pink-500 to-purple-600 bg-clip-text text-transparent">
<ClaudiaLogoMinimal size={56} />
{t("app.welcome")}
</h1>
<p className="text-muted-foreground text-xl">
{t("app.tagline")}
</p>
</motion.div>
{/* Main Feature Cards */}
<div className="grid grid-cols-2 gap-8 mb-12">
{mainFeatures.map((feature, index) => (
<motion.div
key={feature.id}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.4,
delay: 0.1 * index,
type: "spring",
stiffness: 100
}}
>
<BorderGlowCard
className="h-full group"
onClick={() => handleCardClick(feature.view)}
>
<div className="p-10">
<div className="flex items-start gap-6">
<div className={`p-4 ${feature.bgColor} rounded-2xl transition-transform duration-300 group-hover:scale-110 group-hover:rotate-3`}>
<feature.icon className={`h-10 w-10 ${feature.color}`} strokeWidth={1.5} />
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold mb-3 group-hover:text-primary transition-colors">
{feature.title}
</h2>
<p className="text-muted-foreground text-base leading-relaxed">
{feature.subtitle}
</p>
</div>
</div>
</div>
</BorderGlowCard>
</motion.div>
))}
</div>
{/* Bottom Feature Cards */}
<div className="grid grid-cols-4 gap-6 mb-12">
{bottomFeatures.map((feature, index) => (
<motion.div
key={feature.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.4,
delay: 0.3 + 0.05 * index,
type: "spring",
stiffness: 100
}}
>
<BorderGlowCard
className="h-36 group"
onClick={() => handleCardClick(feature.view)}
>
<div className="h-full flex flex-col items-center justify-center p-6">
<div className={`p-3 ${feature.bgColor} rounded-xl mb-3 transition-transform duration-300 group-hover:scale-110 group-hover:rotate-6`}>
<feature.icon className={`h-8 w-8 ${feature.color}`} strokeWidth={1.5} />
</div>
<h3 className="text-sm font-semibold mb-1 group-hover:text-primary transition-colors">
{feature.title}
</h3>
<p className="text-xs text-muted-foreground text-center line-clamp-2">
{feature.subtitle}
</p>
</div>
</BorderGlowCard>
</motion.div>
))}
</div>
{/* Quick Action Button */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.5,
delay: 0.6,
type: "spring",
stiffness: 100
}}
className="flex justify-center"
>
<Button
size="lg"
className="relative px-10 py-7 text-lg font-semibold bg-gradient-to-r from-orange-500 to-pink-500 hover:from-orange-600 hover:to-pink-600 text-white border-0 shadow-2xl hover:shadow-orange-500/25 transition-all duration-300 hover:scale-105 rounded-2xl group overflow-hidden"
onClick={handleButtonClick}
>
{/* Shimmer effect on button */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer" />
</div>
<span className="relative z-10 flex items-center gap-3">
<span className="text-2xl"></span>
{t("welcome.quickStartSession")}
<span className="text-2xl">🚀</span>
</span>
</Button>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,309 @@
import React, { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
interface GlowCardProps {
children: React.ReactNode;
className?: string;
glowClassName?: string;
onClick?: () => void;
}
export const GlowCard: React.FC<GlowCardProps> = ({
children,
className,
glowClassName,
onClick,
}) => {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [isHovered, setIsHovered] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setMousePosition({ x, y });
};
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
};
return (
<div
ref={cardRef}
className={cn(
"relative overflow-hidden rounded-xl transition-all duration-300",
"bg-card hover:shadow-2xl",
className
)}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={onClick}
>
{/* Glow effect layer */}
<div
className={cn(
"absolute inset-0 opacity-0 transition-opacity duration-300",
isHovered && "opacity-100",
"pointer-events-none"
)}
style={{
background: `radial-gradient(600px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(120, 119, 198, 0.1), transparent 40%)`,
}}
/>
{/* Border glow effect */}
<div
className={cn(
"absolute inset-0 rounded-xl opacity-0 transition-opacity duration-300",
isHovered && "opacity-100",
"pointer-events-none"
)}
style={{
background: `radial-gradient(400px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(120, 119, 198, 0.3), transparent 40%)`,
padding: "1px",
maskImage: "linear-gradient(#000, #000)",
maskClip: "content-box, border-box",
maskComposite: "exclude",
WebkitMaskImage: "linear-gradient(#000, #000)",
WebkitMaskClip: "content-box, border-box",
WebkitMaskComposite: "xor",
}}
/>
{/* Animated gradient border */}
<div
className={cn(
"absolute inset-0 rounded-xl",
"bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500",
"opacity-0 transition-opacity duration-500",
isHovered && "opacity-20 animate-pulse",
"pointer-events-none",
glowClassName
)}
style={{
padding: "2px",
maskImage: "linear-gradient(#000, #000)",
maskClip: "content-box, border-box",
maskComposite: "exclude",
WebkitMaskImage: "linear-gradient(#000, #000)",
WebkitMaskClip: "content-box, border-box",
WebkitMaskComposite: "xor",
}}
/>
{/* Content */}
<div className="relative z-10">
{children}
</div>
</div>
);
};
// 高级炫光卡片组件,带有更多动画效果
export const AdvancedGlowCard: React.FC<GlowCardProps> = ({
children,
className,
glowClassName,
onClick,
}) => {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [isHovered, setIsHovered] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const [gradientAngle, setGradientAngle] = useState(0);
useEffect(() => {
if (!isHovered) return;
const interval = setInterval(() => {
setGradientAngle((prev) => (prev + 1) % 360);
}, 50);
return () => clearInterval(interval);
}, [isHovered]);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setMousePosition({ x, y });
};
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
setGradientAngle(0);
};
return (
<div
ref={cardRef}
className={cn(
"relative overflow-hidden rounded-xl transition-all duration-500",
"bg-card/80 backdrop-blur-sm",
"hover:shadow-2xl hover:shadow-primary/20",
"hover:scale-[1.02] hover:-translate-y-1",
"cursor-pointer",
className
)}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={onClick}
>
{/* Main glow effect */}
<div
className={cn(
"absolute inset-0 opacity-0 transition-opacity duration-500",
isHovered && "opacity-100",
"pointer-events-none"
)}
style={{
background: `radial-gradient(800px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(147, 51, 234, 0.15), transparent 40%)`,
}}
/>
{/* Secondary glow effect */}
<div
className={cn(
"absolute inset-0 opacity-0 transition-opacity duration-500",
isHovered && "opacity-60",
"pointer-events-none"
)}
style={{
background: `radial-gradient(600px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(59, 130, 246, 0.1), transparent 40%)`,
}}
/>
{/* Animated gradient border */}
<div
className={cn(
"absolute inset-0 rounded-xl",
"opacity-0 transition-opacity duration-300",
isHovered && "opacity-100",
"pointer-events-none",
glowClassName
)}
style={{
background: `linear-gradient(${gradientAngle}deg, #3b82f6, #8b5cf6, #ec4899, #3b82f6)`,
padding: "2px",
maskImage: "linear-gradient(#000, #000)",
maskClip: "content-box, border-box",
maskComposite: "exclude",
WebkitMaskImage: "linear-gradient(#000, #000)",
WebkitMaskClip: "content-box, border-box",
WebkitMaskComposite: "xor",
}}
/>
{/* Shimmer effect */}
<div
className={cn(
"absolute inset-0 opacity-0",
isHovered && "opacity-100 animate-shimmer",
"pointer-events-none"
)}
style={{
background: "linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.1) 50%, transparent 60%)",
animation: isHovered ? "shimmer 1.5s infinite" : "none",
}}
/>
{/* Content */}
<div className="relative z-10">
{children}
</div>
</div>
);
};
// 简约边框跑马灯卡片组件
export const BorderGlowCard: React.FC<GlowCardProps> = ({
children,
className,
onClick,
}) => {
const [isHovered, setIsHovered] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
};
return (
<div
ref={cardRef}
className={cn(
"relative overflow-hidden rounded-xl transition-all duration-500",
"bg-card",
"hover:shadow-lg hover:shadow-orange-500/10",
"hover:scale-[1.01]",
"cursor-pointer",
className
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={onClick}
>
{/* 橙色边框跑马灯效果 - 只有一小段 */}
<div
className={cn(
"absolute inset-0 rounded-xl",
"opacity-0 transition-opacity duration-300",
isHovered && "opacity-100",
"pointer-events-none"
)}
>
{/* 旋转的渐变边框 - 一小段光带 */}
<div
className={cn(
"absolute inset-0 rounded-xl",
isHovered && "animate-spin-slow"
)}
style={{
background: `conic-gradient(from 0deg,
transparent 0deg,
transparent 85deg,
#fb923c 90deg,
#f97316 95deg,
#fb923c 100deg,
transparent 105deg,
transparent 360deg
)`,
}}
/>
{/* 内部遮罩,只显示边框 */}
<div
className="absolute inset-[2px] rounded-xl bg-card"
/>
</div>
{/* 静态边框 */}
<div className="absolute inset-0 rounded-xl border border-border/50" />
{/* Content */}
<div className="relative z-10">
{children}
</div>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import {initReactI18next} from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// 引入语言资源文件
@@ -8,50 +8,50 @@ import zh from '@/locales/zh/common.json';
// 配置语言检测器
const languageDetectorOptions = {
// 检测顺序
order: ['localStorage', 'navigator', 'htmlTag'],
// 缓存语言到localStorage
caches: ['localStorage'],
// 检查所有可用语言
checkWhitelist: true,
// 检测顺序
order: ['localStorage', 'navigator', 'htmlTag'],
// 缓存语言到localStorage
caches: ['localStorage'],
// 检查所有可用语言
checkWhitelist: true,
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
// 回退语言
fallbackLng: 'en',
// 调试模式(开发环境)
debug: process.env.NODE_ENV === 'development',
// 语言资源
resources: {
en: {
common: en,
},
zh: {
common: zh,
},
},
// 命名空间配置
defaultNS: 'common',
ns: ['common'],
// 语言检测选项
detection: languageDetectorOptions,
// 插值配置
interpolation: {
escapeValue: false, // React 已经默认防止XSS
},
// 白名单支持的语言
supportedLngs: ['en', 'zh'],
// 非显式支持的语言回退到en
nonExplicitSupportedLngs: true,
});
.use(LanguageDetector)
.use(initReactI18next)
.init({
// 回退语言
fallbackLng: 'en',
// 调试模式(开发环境)
debug: process.env.NODE_ENV === 'development',
export default i18n;
// 语言资源
resources: {
en: {
common: en,
},
zh: {
common: zh,
},
},
// 命名空间配置
defaultNS: 'common',
ns: ['common'],
// 语言检测选项
detection: languageDetectorOptions,
// 插值配置
interpolation: {
escapeValue: false, // React 已经默认防止XSS
},
// 白名单支持的语言
supportedLngs: ['en', 'zh'],
// 非显式支持的语言回退到en
nonExplicitSupportedLngs: true,
});
export default i18n;

View File

@@ -1,4 +1,7 @@
{
"pleaseSelectInstallation": "Select Claude installation",
"saveSelection": "Save Selection",
"saving": "Saving...",
"ccProjects": "CC Projects",
"browseClaudeCodeSessions": "Browse your Claude Code sessions",
"newClaudeCodeSession": "New Claude Code session",
@@ -8,6 +11,7 @@
"app": {
"name": "Claudia",
"welcome": "Welcome to Claudia",
"tagline": "Powerful Claude Code session management tool",
"loading": "Loading...",
"error": "Error",
"success": "Success",
@@ -40,6 +44,21 @@
"mcp": "MCP Manager",
"about": "About"
},
"welcome": {
"agentManagement": "Agent Management",
"agentManagementDesc": "Create and manage intelligent agents",
"projectManagement": "Project Management",
"projectManagementDesc": "Browse and manage project sessions",
"usageStatistics": "Usage Statistics",
"usageStatisticsDesc": "View usage and statistics",
"mcpBroker": "MCP Broker",
"mcpBrokerDesc": "Manage MCP servers",
"claudeMd": "CLAUDE.md",
"claudeMdDesc": "Edit Claude configuration files",
"settings": "Settings",
"settingsDesc": "App settings and configuration",
"quickStartSession": "Quick Start New Session"
},
"projects": {
"title": "Projects",
"noProjects": "No projects found",
@@ -58,8 +77,24 @@
"editAgent": "Edit Agent",
"deleteAgent": "Delete Agent",
"executeAgent": "Execute Agent",
"agentName": "Agent Name",
"agentNameRequired": "Agent name is required",
"agentIcon": "Agent Icon",
"chooseAnIcon": "Choose an icon",
"searchIcons": "Search icons...",
"noIconsFound": "No icons found for",
"clickToSelect": "Click an icon to select",
"iconsAvailable": "icons available",
"iconCategories": {
"interfaceNavigation": "Interface & Navigation",
"developmentTech": "Development & Tech",
"businessFinance": "Business & Finance",
"creativeDesign": "Creative & Design",
"natureScience": "Nature & Science",
"gamingEntertainment": "Gaming & Entertainment",
"communication": "Communication",
"miscellaneous": "Miscellaneous"
},
"systemPrompt": "System Prompt",
"systemPromptRequired": "System prompt is required",
"defaultTask": "Default Task",
@@ -110,7 +145,10 @@
"noAgentsAvailable": "No agents available",
"availableAgents": "Available Agents",
"runningAgents": "Running Agents",
"createFirstAgentToGetStarted": "Create your first agent to get started"
"createFirstAgentToGetStarted": "Create your first agent to get started",
"noRunningAgents": "No running agents",
"agentExecutionsWillAppear": "Agent executions will appear here when started",
"view": "View"
},
"slashCommands": {
"slashCommands": "Slash Commands",
@@ -131,7 +169,46 @@
"hooksConfiguration": "Hooks Configuration",
"configureShellCommands": "Configure shell commands to execute at various points in Claude Code's lifecycle.",
"localSettingsNote": " These settings are not committed to version control.",
"unsavedChanges": "You have unsaved changes. Click Save to persist them."
"unsavedChanges": "You have unsaved changes. Click Save to persist them.",
"preToolUse": "Pre Tool Use",
"postToolUse": "Post Tool Use",
"notification": "Notification",
"stop": "Stop",
"subagentStop": "Subagent Stop",
"runsBeforeToolCalls": "Runs before tool calls, can block and provide feedback",
"runsAfterToolCompletion": "Runs after successful tool completion",
"customizesNotifications": "Customizes notifications when Claude needs attention",
"runsWhenClaudeFinishes": "Runs when Claude finishes responding",
"runsWhenSubagentFinishes": "Runs when a Claude subagent (Task) finishes",
"noHooksConfigured": "No hooks configured for this event",
"userScope": "User Scope",
"projectScope": "Project Scope",
"localScope": "Local Scope",
"templates": "Templates",
"addHook": "Add Hook",
"addAnotherMatcher": "Add Another Matcher",
"addAnotherCommand": "Add Another Command",
"pattern": "Pattern",
"commonPatterns": "Common patterns",
"commands": "Commands",
"addCommand": "Add Command",
"hookTemplates": "Hook Templates",
"quickStartTemplates": "Quick start with preconfigured templates",
"validationErrors": "Validation Errors",
"securityWarnings": "Security Warnings",
"saving": "Saving...",
"save": "Save",
"loadingHooksConfiguration": "Loading hooks configuration...",
"toolNamePatternTooltip": "Tool name pattern (regex supported). Leave empty to match all tools.",
"seconds": "seconds",
"timeout": "Timeout",
"command": "Command",
"block": "Block",
"addCondition": "Add Condition",
"condition": "Condition",
"matchesRegex": "Matches regex",
"message": "Message",
"enterShellCommand": "Enter shell command..."
},
"settings": {
"title": "Settings",
@@ -195,13 +272,29 @@
"chatRetention": "Chat Transcript Retention (days)",
"chatRetentionDesc": "How long to retain chat transcripts locally (default: 30 days)",
"claudeCodeInstallation": "Claude Code Installation",
"choosePreferredInstallation": "Choose your preferred Claude Code installation.",
"loadingAvailableInstallations": "Loading available installations...",
"errorLoadingInstallations": "Error loading installations",
"availableInstallations": "Available Installations",
"choosePreferredInstallation": "Choose your preferred Claude Code installation.",
"loadingAvailableInstallations": "Loading available installations...",
"errorLoadingInstallations": "Error loading installations",
"availableInstallations": "Available Installations",
"claudeCodeInstallationDesc": "Select which Claude Code installation to use.",
"binaryPathChanged": "⚠️ Claude binary path has been changed. Remember to save your settings."
},
"placeholders": {
"envVarKey": "KEY",
"envVarValue": "value",
"allowRuleExample": "e.g., Bash(npm run test:*)",
"denyRuleExample": "e.g., Bash(curl:*)",
"apiKeyHelperPath": "/path/to/generate_api_key.sh"
},
"system": "System",
"custom": "Custom",
"systemInstallations": "System Installations",
"customInstallations": "Custom Installations",
"selectedInstallation": "Selected Installation",
"path": "Path",
"source": "Source",
"version": "Version",
"versionUnknown": "Version unknown",
"permissions": {
"permissionRules": "Permission Rules",
"permissionRulesDesc": "Control which tools Claude Code can use without manual approval",
@@ -257,8 +350,8 @@
"analyticsEnabled": "Analytics enabled",
"analyticsDisabled": "Analytics disabled",
"allDataDeleted": "All analytics data deleted",
"helpImproveClaudia": "Help Improve Claudia",
"collectAnonymousData": "We'd like to collect anonymous usage data to improve your experience.",
"helpImproveClaudia": "Help improve Claudia",
"collectAnonymousData": "We collect anonymous usage data to improve your experience. No personal data is collected.",
"featureUsageDesc": "Feature usage (which tools and commands you use)",
"performanceMetricsDesc": "Performance metrics (app speed and reliability)",
"errorReportsDesc": "Error reports (to fix bugs and improve stability)",
@@ -289,6 +382,7 @@
},
"mcp": {
"title": "MCP Server Management",
"servers": "Servers",
"addServer": "Add Server",
"serverName": "Server Name",
"serverCommand": "Server Command",
@@ -298,7 +392,72 @@
"connectionFailed": "Connection failed",
"importFromClaude": "Import from Claude Desktop",
"exportConfig": "Export Configuration",
"noServers": "No MCP servers configured"
"noServers": "No MCP servers configured",
"importExport": "Import & Export",
"importExportDescription": "Import MCP servers from other sources or export your configuration",
"importScope": "Import Scope",
"localProjectOnly": "Local (this project only)",
"projectShared": "Project (shared via .mcp.json)",
"userAllProjects": "User (all projects)",
"chooseImportLocation": "Choose where to save imported servers from JSON files",
"importFromClaudeDesktop": "Import from Claude Desktop",
"importFromClaudeDesktopDescription": "Automatically imports all MCP servers from Claude Desktop. Installs to user scope (available across all projects).",
"importing": "Importing...",
"importFromJSON": "Import from JSON",
"importFromJSONDescription": "Import server configuration from a JSON file",
"chooseJSONFile": "Choose JSON File",
"exportConfiguration": "Export Configuration",
"exportConfigurationDescription": "Export your MCP server configuration",
"exportComingSoon": "Export (Coming Soon)",
"useClaudeCodeAsMCPServer": "Use Claude Code as MCP Server",
"useClaudeCodeAsMCPServerDescription": "Start Claude Code as an MCP server that other applications can connect to",
"startMCPServer": "Start MCP Server",
"jsonFormatExamples": "JSON Format Examples",
"singleServer": "Single server",
"multipleServers": "Multiple servers (.mcp.json format)",
"environmentVariablesCount": "Environment variables: {{count}}",
"serversCount": "{{count}} servers configured",
"mcpServerAdded": "MCP server added successfully!",
"serverRemovedSuccess": "Server \"{{name}}\" removed successfully!",
"importedServersSuccess": "Successfully imported {{count}} servers!",
"importedServersFailed": "Imported {{imported}} servers, {{failed}} failed",
"loadMcpServersFailed": "Failed to load MCP servers. Make sure Claude Code is installed.",
"addMcpServer": "Add MCP Server",
"configureNewMcpServer": "Configure a new Model Context Protocol server",
"serverNameRequired": "Server name is required",
"commandRequired": "Command is required",
"urlRequired": "URL is required",
"failedToAddServer": "Failed to add server",
"environmentVariables": "Environment Variables",
"addVariable": "Add Variable",
"command": "Command",
"argumentsOptional": "Arguments (optional)",
"scope": "Scope",
"localProjectOnly": "Local (this project only)",
"projectSharedViaMcp": "Project (shared via .mcp.json)",
"addingServer": "Adding Server...",
"addStdioServer": "Add Stdio Server",
"url": "URL",
"addSseServer": "Add SSE Server",
"exampleCommands": "Example Commands",
"uniqueNameToIdentify": "A unique name to identify this server",
"commandToExecuteServer": "The command to execute the server",
"spaceSeparatedArgs": "Space-separated command arguments",
"sseEndpointUrl": "The SSE endpoint URL",
"running": "Running",
"showFull": "Show full",
"hide": "Hide",
"copied": "Copied!",
"copy": "Copy",
"arguments": "Arguments",
"configuredServers": "Configured Servers",
"serversConfigured": "configured",
"refresh": "Refresh",
"noMcpServersConfigured": "No MCP servers configured",
"addServerToGetStarted": "Add a server to get started with Model Context Protocol",
"localProjectSpecific": "Local (Project-specific)",
"projectSharedMcp": "Project (Shared via .mcp.json)",
"userAllProjects": "User (All projects)"
},
"usage": {
"title": "Usage Dashboard",
@@ -398,7 +557,9 @@
"addServerToGetStarted": "Add a server to get started with Model Context Protocol",
"localProjectSpecific": "Local (Project-specific)",
"projectSharedMcp": "Project (Shared via .mcp.json)",
"userAllProjects": "User (All projects)",
"userAllProjects": "User (All projects)"
},
"messages": {
"welcomeToClaudia": "Welcome to Claudia",
"backToHome": "Back to Home",
"noProjectsFound": "No projects found in ~/.claude/projects",
@@ -421,7 +582,41 @@
"loadFileFailed": "Failed to load CLAUDE.md file",
"editProjectSpecificPrompt": "Edit project-specific Claude Code system prompt",
"maximumTabsReached": "Maximum number of tabs ({{max}}) reached",
"saving": "Saving..."
"saving": "Saving...",
"saveSuccess": "Saved successfully",
"deleteSuccess": "Deleted successfully",
"operationFailed": "Operation failed",
"confirmAction": "Are you sure you want to perform this action?",
"unsavedChanges": "You have unsaved changes",
"networkError": "Network error occurred",
"unknownError": "Unknown error occurred",
"claudeCodeNotFound": "Claude Code not found",
"selectClaudeInstallation": "Select Claude Installation",
"installClaudeCode": "Install Claude Code",
"noTabsOpen": "No tabs open",
"clickPlusToStartChat": "Click the + button to start a new chat",
"noAgentRunIdSpecified": "No agent run ID specified",
"noClaudeFileIdSpecified": "No Claude file ID specified",
"claudeFileEditorNotImplemented": "Claude file editor not yet implemented in tabs",
"noAgentDataSpecified": "No agent data specified",
"importAgentComingSoon": "Import agent functionality coming soon...",
"unknownTabType": "Unknown tab type",
"letClaudeDecide": "Let Claude decide",
"basicReasoning": "Basic reasoning",
"deeperAnalysis": "Deeper analysis",
"extensiveReasoning": "Extensive reasoning",
"maximumAnalysis": "Maximum analysis",
"typeYourPromptHere": "Type your prompt here...",
"dropImagesHere": "Drop images here...",
"askClaudeAnything": "Ask Claude anything...",
"usage": "Usage",
"mcpServersTitle": "MCP Servers",
"claudemdTitle": "CLAUDE.md",
"runAgent": "Run: {{name}}",
"createAgent": "Create Agent",
"importAgent": "Import Agent",
"unsavedChangesCloseConfirm": "Tab \"{{title}}\" has unsaved changes. Close anyway?",
"session": "Session"
},
"checkpoint": {
"title": "Checkpoints",
@@ -477,41 +672,35 @@
"minLength": "Minimum {{count}} characters required",
"maxLength": "Maximum {{count}} characters allowed"
},
"messages": {
"saveSuccess": "Saved successfully",
"deleteSuccess": "Deleted successfully",
"operationFailed": "Operation failed",
"confirmAction": "Are you sure you want to perform this action?",
"unsavedChanges": "You have unsaved changes",
"networkError": "Network error occurred",
"unknownError": "Unknown error occurred",
"claudeCodeNotFound": "Claude Code not found",
"selectClaudeInstallation": "Select Claude Installation",
"installClaudeCode": "Install Claude Code",
"noTabsOpen": "No tabs open",
"clickPlusToStartChat": "Click the + button to start a new chat",
"noAgentRunIdSpecified": "No agent run ID specified",
"noClaudeFileIdSpecified": "No Claude file ID specified",
"claudeFileEditorNotImplemented": "Claude file editor not yet implemented in tabs",
"noAgentDataSpecified": "No agent data specified",
"importAgentComingSoon": "Import agent functionality coming soon...",
"unknownTabType": "Unknown tab type",
"letClaudeDecide": "Let Claude decide",
"basicReasoning": "Basic reasoning",
"deeperAnalysis": "Deeper analysis",
"extensiveReasoning": "Extensive reasoning",
"maximumAnalysis": "Maximum analysis",
"typeYourPromptHere": "Type your prompt here...",
"dropImagesHere": "Drop images here...",
"askClaudeAnything": "Ask Claude anything...",
"usage": "Usage",
"mcpServersTitle": "MCP Servers",
"claudemdTitle": "CLAUDE.md",
"runAgent": "Run: {{name}}",
"createAgent": "Create Agent",
"importAgent": "Import Agent",
"unsavedChangesCloseConfirm": "Tab \"{{title}}\" has unsaved changes. Close anyway?",
"session": "Session"
"errorBoundary": {
"somethingWentWrong": "Something went wrong",
"errorOccurred": "An error occurred while rendering this component.",
"errorDetails": "Error details",
"tryAgain": "Try again"
},
"webview": {
"preview": "Preview",
"exitFullScreen": "Exit full screen (ESC)",
"enterFullScreen": "Enter full screen",
"enterUrl": "Enter URL...",
"loadingPreview": "Loading preview...",
"failedToLoad": "Failed to load preview",
"pageCouldNotLoad": "The page could not be loaded. Please check the URL and try again.",
"invalidUrl": "Invalid URL",
"enterUrlToPreview": "Enter a URL to preview",
"enterUrlDescription": "Enter a URL in the address bar above to preview a website.",
"tryEntering": "Try entering",
"orAnyOtherUrl": "or any other URL",
"unknownTime": "Unknown time",
"sessions": "Sessions",
"selectProjectDirectory": "Select Project Directory",
"failedToSendPrompt": "Failed to send prompt",
"sessionOutputCopiedJsonl": "Session output copied as JSONL",
"sessionOutputCopiedMarkdown": "Session output copied as Markdown",
"failedToCopy": "Failed to copy",
"forkSession": "Fork Session from Checkpoint",
"newSessionName": "New Session Name",
"enterNameForForkedSession": "Enter a name for the forked session"
},
"input": {
"pressEnterToSend": "Press Enter to send, Shift+Enter for new line",

View File

@@ -1,4 +1,7 @@
{
"pleaseSelectInstallation": "请选择 Claude 安装",
"saveSelection": "保存选择",
"saving": "保存中...",
"ccProjects": "Claude Code 项目",
"browseClaudeCodeSessions": "浏览您的 Claude Code 会话",
"newClaudeCodeSession": "新建 Claude Code 会话",
@@ -8,6 +11,7 @@
"app": {
"name": "Claudia",
"welcome": "欢迎使用 Claudia",
"tagline": "强大的 Claude Code 会话管理工具",
"loading": "加载中...",
"error": "错误",
"success": "成功",
@@ -37,6 +41,21 @@
"mcp": "MCP 管理器",
"about": "关于"
},
"welcome": {
"agentManagement": "Agent 管理",
"agentManagementDesc": "创建和管理智能 Agent",
"projectManagement": "项目管理",
"projectManagementDesc": "浏览和管理项目会话",
"usageStatistics": "使用统计",
"usageStatisticsDesc": "查看使用情况和统计",
"mcpBroker": "MCP 管理",
"mcpBrokerDesc": "管理 MCP 服务器",
"claudeMd": "CLAUDE.md",
"claudeMdDesc": "编辑 Claude 配置文件",
"settings": "设置",
"settingsDesc": "应用设置和配置",
"quickStartSession": "快速开始新会话"
},
"projects": {
"title": "项目",
"noProjects": "未找到项目",
@@ -55,8 +74,24 @@
"editAgent": "编辑智能体",
"deleteAgent": "删除智能体",
"executeAgent": "执行智能体",
"agentName": "智能体名称",
"agentNameRequired": "代理名称为必填项",
"agentIcon": "智能体图标",
"chooseAnIcon": "选择图标",
"searchIcons": "搜索图标...",
"noIconsFound": "未找到图标",
"clickToSelect": "点击选择图标",
"iconsAvailable": "个可用图标",
"iconCategories": {
"interfaceNavigation": "界面与导航",
"developmentTech": "开发与技术",
"businessFinance": "商业与金融",
"creativeDesign": "创意与设计",
"natureScience": "自然与科学",
"gamingEntertainment": "游戏与娱乐",
"communication": "通讯",
"miscellaneous": "其他"
},
"systemPrompt": "系统提示",
"systemPromptRequired": "系统提示为必填项",
"defaultTask": "默认任务",
@@ -107,7 +142,10 @@
"noAgentsAvailable": "无可用代理",
"availableAgents": "可用代理",
"runningAgents": "运行中的代理",
"createFirstAgentToGetStarted": "创建您的第一个代理开始使用"
"createFirstAgentToGetStarted": "创建您的第一个代理开始使用",
"noRunningAgents": "无运行中的智能体",
"agentExecutionsWillAppear": "智能体执行将在启动后显示在这里",
"view": "查看"
},
"slashCommands": {
"slashCommands": "斜杠命令",
@@ -128,7 +166,46 @@
"hooksConfiguration": "钩子配置",
"configureShellCommands": "配置在 Claude Code 生命周期的各个阶段执行的 shell 命令。",
"localSettingsNote": " 这些设置不会提交到版本控制中。",
"unsavedChanges": "您有未保存的更改。点击保存以持久化它们。"
"unsavedChanges": "您有未保存的更改。点击保存以持久化它们。",
"preToolUse": "工具使用前",
"postToolUse": "工具使用后",
"notification": "通知",
"stop": "停止",
"subagentStop": "子代理停止",
"runsBeforeToolCalls": "在工具调用前运行,可以阻止并提供反馈",
"runsAfterToolCompletion": "在工具成功完成后运行",
"customizesNotifications": "自定义 Claude 需要注意时的通知",
"runsWhenClaudeFinishes": "在 Claude 完成响应时运行",
"runsWhenSubagentFinishes": "在 Claude 子代理(任务)完成时运行",
"noHooksConfigured": "此事件未配置钩子",
"userScope": "用户范围",
"projectScope": "项目范围",
"localScope": "本地范围",
"templates": "模板",
"addHook": "添加钩子",
"addAnotherMatcher": "添加另一个匹配器",
"addAnotherCommand": "添加另一个命令",
"pattern": "模式",
"commonPatterns": "常用模式",
"commands": "命令",
"addCommand": "添加命令",
"hookTemplates": "钩子模板",
"quickStartTemplates": "使用预配置模板快速开始",
"validationErrors": "验证错误",
"securityWarnings": "安全警告",
"saving": "保存中...",
"save": "保存",
"loadingHooksConfiguration": "加载钩子配置中...",
"toolNamePatternTooltip": "工具名称模式(支持正则表达式)。留空以匹配所有工具。",
"seconds": "秒",
"timeout": "超时",
"command": "命令",
"block": "阻止",
"addCondition": "添加条件",
"condition": "条件",
"matchesRegex": "匹配正则表达式",
"message": "消息",
"enterShellCommand": "输入 shell 命令..."
},
"settings": {
"title": "设置",
@@ -192,10 +269,10 @@
"chatRetention": "聊天记录保留期 (天)",
"chatRetentionDesc": "本地保留聊天记录的时长默认30 天)",
"claudeCodeInstallation": "Claude Code 安装",
"choosePreferredInstallation": "选择您偏好的 Claude Code 安装。",
"loadingAvailableInstallations": "正在加载可用安装...",
"errorLoadingInstallations": "加载安装时出错",
"availableInstallations": "可用安装",
"choosePreferredInstallation": "选择您偏好的 Claude Code 安装。",
"loadingAvailableInstallations": "正在加载可用安装...",
"errorLoadingInstallations": "加载安装时出错",
"availableInstallations": "可用安装",
"claudeCodeInstallationDesc": "选择要使用的 Claude Code 安装。",
"binaryPathChanged": "⚠️ Claude 二进制路径已更改。请记住保存您的设置。"
},
@@ -255,7 +332,7 @@
"analyticsDisabled": "分析已禁用",
"allDataDeleted": "所有分析数据已删除",
"helpImproveClaudia": "帮助改善 Claudia",
"collectAnonymousData": "我们希望收集匿名使用数据以改善您的使用体验。",
"collectAnonymousData": "我们收集匿名使用数据以改善您的体验。不收集个人数据。",
"featureUsageDesc": "功能使用(您使用的工具和命令)",
"performanceMetricsDesc": "性能指标(应用速度和可靠性)",
"errorReportsDesc": "错误报告(用于修复错误和提高稳定性)",
@@ -282,10 +359,20 @@
"allowRuleExample": "例如Bash(npm run test:*)",
"denyRuleExample": "例如Bash(curl:*)",
"apiKeyHelperPath": "/path/to/generate_api_key.sh"
}
},
"system": "系统",
"custom": "自定义",
"systemInstallations": "系统安装",
"customInstallations": "自定义安装",
"selectedInstallation": "已选择的安装",
"path": "路径",
"source": "来源",
"version": "版本",
"versionUnknown": "版本未知"
},
"mcp": {
"title": "MCP 服务器管理",
"servers": "服务器",
"addServer": "添加服务器",
"serverName": "服务器名称",
"serverCommand": "服务器命令",
@@ -295,7 +382,71 @@
"connectionFailed": "连接失败",
"importFromClaude": "从 Claude Desktop 导入",
"exportConfig": "导出配置",
"noServers": "未配置 MCP 服务器"
"noServers": "未配置 MCP 服务器",
"importExport": "导入与导出",
"importExportDescription": "从其他来源导入 MCP 服务器或导出您的配置",
"importScope": "导入范围",
"localProjectOnly": "本地(仅此项目)",
"projectShared": "项目(通过 .mcp.json 共享)",
"userAllProjects": "用户(所有项目)",
"chooseImportLocation": "选择从 JSON 文件导入的服务器保存位置",
"importFromClaudeDesktop": "从 Claude Desktop 导入",
"importFromClaudeDesktopDescription": "自动导入 Claude Desktop 中的所有 MCP 服务器。安装到用户范围(所有项目可用)。",
"importing": "导入中...",
"importFromJSON": "从 JSON 导入",
"importFromJSONDescription": "从 JSON 文件导入服务器配置",
"chooseJSONFile": "选择 JSON 文件",
"exportConfiguration": "导出配置",
"exportConfigurationDescription": "导出您的 MCP 服务器配置",
"exportComingSoon": "导出(即将推出)",
"useClaudeCodeAsMCPServer": "将 Claude Code 用作 MCP 服务器",
"useClaudeCodeAsMCPServerDescription": "启动 Claude Code 作为 MCP 服务器,其他应用程序可以连接到它",
"startMCPServer": "启动 MCP 服务器",
"jsonFormatExamples": "JSON 格式示例",
"singleServer": "单个服务器",
"multipleServers": "多个服务器(.mcp.json 格式)",
"environmentVariablesCount": "环境变量:{{count}} 个",
"serversCount": "已配置 {{count}} 个服务器",
"mcpServerAdded": "MCP 服务器添加成功!",
"serverRemovedSuccess": "服务器 \"{{name}}\" 删除成功!",
"importedServersSuccess": "成功导入 {{count}} 个服务器!",
"importedServersFailed": "导入 {{imported}} 个服务器,{{failed}} 个失败",
"loadMcpServersFailed": "加载 MCP 服务器失败。请确保 Claude Code 已安装。",
"addMcpServer": "添加 MCP 服务器",
"configureNewMcpServer": "配置新的模型上下文协议服务器",
"serverNameRequired": "服务器名称为必填项",
"commandRequired": "命令为必填项",
"urlRequired": "URL为必填项",
"failedToAddServer": "添加服务器失败",
"environmentVariables": "环境变量",
"addVariable": "添加变量",
"command": "命令",
"argumentsOptional": "参数(可选)",
"scope": "作用域",
"localProjectOnly": "本地(仅此项目)",
"projectSharedViaMcp": "项目(通过 .mcp.json 共享)",
"addingServer": "添加服务器中...",
"addStdioServer": "添加 Stdio 服务器",
"url": "URL",
"addSseServer": "添加 SSE 服务器",
"exampleCommands": "示例命令",
"uniqueNameToIdentify": "用于标识此服务器的唯一名称",
"commandToExecuteServer": "执行服务器的命令",
"spaceSeparatedArgs": "空格分隔的命令参数",
"sseEndpointUrl": "SSE 端点 URL",
"running": "运行中",
"showFull": "显示全部",
"hide": "隐藏",
"copied": "已复制",
"copy": "复制",
"arguments": "参数",
"configuredServers": "已配置的服务器",
"serversConfigured": "个服务器已配置",
"refresh": "刷新",
"noMcpServersConfigured": "未配置 MCP 服务器",
"addServerToGetStarted": "添加一个服务器开始使用模型上下文协议",
"localProjectSpecific": "本地(项目特定)",
"projectSharedMcp": "项目(通过 .mcp.json 共享)"
},
"usage": {
"title": "用量仪表板",
@@ -348,68 +499,9 @@
"saveClaudemdFailed": "保存 CLAUDE.md 文件失败",
"claudemdSavedSuccess": "CLAUDE.md 保存成功",
"saveClaudemd": "保存 CLAUDE.md",
"unsavedChangesConfirm": "您有未保存的更改。确定要离开吗?",
"servers": "服务器",
"addServer": "添加服务器",
"importExport": "导入/导出",
"mcpServerAdded": "MCP 服务器添加成功!",
"serverRemovedSuccess": "服务器 \"{{name}}\" 删除成功!",
"importedServersSuccess": "成功导入 {{count}} 个服务器!",
"importedServersFailed": "导入 {{imported}} 个服务器,{{failed}} 个失败",
"loadMcpServersFailed": "加载 MCP 服务器失败。请确保 Claude Code 已安装。",
"addMcpServer": "添加 MCP 服务器",
"configureNewMcpServer": "配置新的模型上下文协议服务器",
"serverNameRequired": "服务器名称为必填项",
"commandRequired": "命令为必填项",
"urlRequired": "URL为必填项",
"failedToAddServer": "添加服务器失败",
"environmentVariables": "环境变量",
"addVariable": "添加变量",
"serverName": "服务器名称",
"command": "命令",
"argumentsOptional": "参数(可选)",
"scope": "作用域",
"localProjectOnly": "本地(仅此项目)",
"projectSharedViaMcp": "项目(通过 .mcp.json 共享)",
"addingServer": "添加服务器中...",
"addStdioServer": "添加 Stdio 服务器",
"url": "URL",
"addSseServer": "添加 SSE 服务器",
"exampleCommands": "示例命令",
"uniqueNameToIdentify": "用于标识此服务器的唯一名称",
"commandToExecuteServer": "执行服务器的命令",
"spaceSeparatedArgs": "空格分隔的命令参数",
"sseEndpointUrl": "SSE 端点 URL",
"running": "运行中",
"showFull": "显示全部",
"hide": "隐藏",
"copied": "已复制",
"copy": "复制",
"arguments": "参数",
"environmentVariables": "环境变量",
"configuredServers": "已配置的服务器",
"serversConfigured": "个服务器已配置",
"refresh": "刷新",
"noMcpServersConfigured": "未配置 MCP 服务器",
"addServerToGetStarted": "添加一个服务器开始使用模型上下文协议",
"localProjectSpecific": "本地(项目特定)",
"projectSharedMcp": "项目(通过 .mcp.json 共享)",
"userAllProjects": "用户(所有项目)",
"letClaudeDecide": "让 Claude 决定",
"basicReasoning": "基础推理",
"deeperAnalysis": "更深入的分析",
"extensiveReasoning": "广泛推理",
"maximumAnalysis": "最大化分析",
"typeYourPromptHere": "在此输入您的提示...",
"dropImagesHere": "在此拖放图像...",
"askClaudeAnything": "向 Claude 提问任何问题...",
"usage": "用量",
"mcpServersTitle": "MCP 服务器",
"claudemdTitle": "CLAUDE.md",
"runAgent": "运行: {{name}}",
"createAgent": "创建智能体",
"importAgent": "导入智能体",
"unsavedChangesCloseConfirm": "标签页 \"{{title}}\" 有未保存的更改。仍要关闭吗?",
"unsavedChangesConfirm": "您有未保存的更改。确定要离开吗?"
},
"messages": {
"welcomeToClaudia": "欢迎使用 Claudia",
"backToHome": "返回首页",
"noProjectsFound": "在 ~/.claude/projects 中未找到项目",
@@ -440,7 +532,18 @@
"loadFileFailed": "加载 CLAUDE.md 文件失败",
"editProjectSpecificPrompt": "编辑项目特定的 Claude Code 系统提示",
"maximumTabsReached": "已达到最大标签页数量 ({{max}})",
"saving": "保存中..."
"saving": "保存中...",
"saveSuccess": "保存成功",
"deleteSuccess": "删除成功",
"operationFailed": "操作失败",
"confirmAction": "确认要执行此操作吗?",
"unsavedChanges": "您有未保存的更改",
"networkError": "网络错误",
"unknownError": "未知错误",
"claudeCodeNotFound": "未找到 Claude Code",
"selectClaudeInstallation": "选择 Claude 安装",
"installClaudeCode": "安装 Claude Code",
"session": "会话"
},
"checkpoint": {
"title": "检查点",
@@ -451,11 +554,6 @@
"checkpointMessage": "检查点消息",
"timeline": "时间线",
"diff": "差异",
"deleteCheckpoint": "删除检查点",
"checkpointName": "检查点名称",
"checkpointMessage": "检查点消息",
"timeline": "时间线",
"diff": "差异",
"noCheckpoints": "未找到检查点",
"checkpointSettingsTitle": "检查点设置",
"experimentalFeature": "实验性功能",
@@ -501,18 +599,35 @@
"minLength": "至少需要 {{count}} 个字符",
"maxLength": "最多允许 {{count}} 个字符"
},
"messages": {
"saveSuccess": "保存成功",
"deleteSuccess": "删除成功",
"operationFailed": "操作失败",
"confirmAction": "确认要执行此操作吗?",
"unsavedChanges": "您有未保存的更改",
"networkError": "网络错误",
"unknownError": "未知错误",
"claudeCodeNotFound": "未找到 Claude Code",
"selectClaudeInstallation": "选择 Claude 安装",
"installClaudeCode": "安装 Claude Code",
"session": "会话"
"errorBoundary": {
"somethingWentWrong": "出错了",
"errorOccurred": "渲染此组件时发生错误。",
"errorDetails": "错误详情",
"tryAgain": "重试"
},
"webview": {
"preview": "预览",
"exitFullScreen": "退出全屏 (ESC)",
"enterFullScreen": "进入全屏",
"enterUrl": "输入 URL...",
"loadingPreview": "加载预览中...",
"failedToLoad": "加载预览失败",
"pageCouldNotLoad": "页面无法加载。请检查 URL 并重试。",
"invalidUrl": "无效的 URL",
"enterUrlToPreview": "输入 URL 进行预览",
"enterUrlDescription": "在上方地址栏输入 URL 以预览网站。",
"tryEntering": "尝试输入",
"orAnyOtherUrl": "或任何其他 URL",
"unknownTime": "未知时间",
"sessions": "会话",
"selectProjectDirectory": "选择项目目录",
"failedToSendPrompt": "发送提示失败",
"sessionOutputCopiedJsonl": "会话输出已复制为 JSONL",
"sessionOutputCopiedMarkdown": "会话输出已复制为 Markdown",
"failedToCopy": "复制失败",
"forkSession": "从检查点分叉会话",
"newSessionName": "新会话名称",
"enterNameForForkedSession": "输入分叉会话的名称"
},
"input": {
"pressEnterToSend": "按 Enter 发送Shift+Enter 换行",

View File

@@ -10,6 +10,21 @@
display: none;
}
/* Custom animations */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Animation utilities */
.animate-spin-slow {
animation: spin 3s linear infinite;
}
/* Dark theme configuration */
@theme {
/* Colors */
@@ -45,8 +60,8 @@
--radius-xl: 1rem;
/* Fonts */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
--font-sans: "Maple Mono", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
--font-mono: "Maple Mono", ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
/* Transitions */
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
@@ -56,26 +71,26 @@
/* Theme Variations */
/* Default is dark theme - already defined above */
/* Light Theme */
/* Light Theme - Enhanced Contrast */
.theme-light {
--color-background: oklch(0.98 0.01 240);
--color-foreground: oklch(0.12 0.01 240);
--color-card: oklch(0.96 0.01 240);
--color-card-foreground: oklch(0.12 0.01 240);
--color-popover: oklch(0.98 0.01 240);
--color-popover-foreground: oklch(0.12 0.01 240);
--color-primary: oklch(0.12 0.01 240);
--color-primary-foreground: oklch(0.98 0.01 240);
--color-secondary: oklch(0.94 0.01 240);
--color-secondary-foreground: oklch(0.12 0.01 240);
--color-muted: oklch(0.94 0.01 240);
--color-background: oklch(0.99 0.005 240);
--color-foreground: oklch(0.08 0.01 240);
--color-card: oklch(0.97 0.005 240);
--color-card-foreground: oklch(0.08 0.01 240);
--color-popover: oklch(0.99 0.005 240);
--color-popover-foreground: oklch(0.08 0.01 240);
--color-primary: oklch(0.08 0.01 240);
--color-primary-foreground: oklch(0.99 0.005 240);
--color-secondary: oklch(0.95 0.005 240);
--color-secondary-foreground: oklch(0.08 0.01 240);
--color-muted: oklch(0.95 0.005 240);
--color-muted-foreground: oklch(0.45 0.01 240);
--color-accent: oklch(0.94 0.01 240);
--color-accent-foreground: oklch(0.12 0.01 240);
--color-accent: oklch(0.95 0.005 240);
--color-accent-foreground: oklch(0.08 0.01 240);
--color-destructive: oklch(0.6 0.2 25);
--color-destructive-foreground: oklch(0.98 0.01 240);
--color-border: oklch(0.90 0.01 240);
--color-input: oklch(0.90 0.01 240);
--color-destructive-foreground: oklch(0.99 0.005 240);
--color-border: oklch(0.85 0.005 240);
--color-input: oklch(0.85 0.005 240);
--color-ring: oklch(0.52 0.015 240);
/* Additional colors for status messages */
@@ -155,6 +170,11 @@ body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-sans);
/* Optimize Maple Mono rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "liga" 1, "calt" 1;
letter-spacing: -0.01em;
}
/* Placeholder text styling */
@@ -591,6 +611,12 @@ button:focus-visible,
}
}
/* Animation for shimmer effect */
.animate-shimmer {
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.shimmer-hover {
position: relative;
overflow: hidden;
@@ -776,4 +802,34 @@ code::-webkit-scrollbar-thumb:hover {
.image-move-to-input {
animation: moveToInput 0.8s ease-in-out forwards;
}
/* Glass Morphism Effects - Enhanced for better contrast */
.glass-panel {
background: var(--color-background);
background: color-mix(in srgb, var(--color-background) 98%, transparent);
backdrop-filter: blur(12px) saturate(180%);
-webkit-backdrop-filter: blur(12px) saturate(180%);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
box-shadow:
0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1),
inset 0 1px 0 0 rgb(255 255 255 / 0.05);
}
.theme-light .glass-panel {
background: color-mix(in srgb, var(--color-background) 85%, transparent);
box-shadow:
0 2px 8px 0 rgb(0 0 0 / 0.08),
0 1px 3px -1px rgb(0 0 0 / 0.06),
inset 0 1px 0 0 rgb(255 255 255 / 0.8);
}
/* Enhanced text contrast for light theme */
.theme-light .text-muted-foreground {
color: oklch(0.45 0.01 240);
}
.theme-light .hover\:text-muted-foreground:hover {
color: oklch(0.40 0.01 240);
}