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="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" /> <meta name="color-scheme" content="dark" />
<title>Claudia - Claude Code Session Browser</title> <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> </head>
<body> <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": [ "windows": [
{ {
"title": "Claudia", "title": "Claudia",
"width": 800, "width": 1600,
"height": 600 "height": 1200
} }
], ],
"security": { "security": {

View File

@@ -1,12 +1,11 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion"; 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 { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api";
import { OutputCacheProvider } from "@/lib/outputCache"; import { OutputCacheProvider } from "@/lib/outputCache";
import { TabProvider } from "@/contexts/TabContext"; import { TabProvider } from "@/contexts/TabContext";
import { ThemeProvider } from "@/contexts/ThemeContext"; import { ThemeProvider } from "@/contexts/ThemeContext";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ProjectList } from "@/components/ProjectList"; import { ProjectList } from "@/components/ProjectList";
import { SessionList } from "@/components/SessionList"; import { SessionList } from "@/components/SessionList";
import { RunningClaudeSessions } from "@/components/RunningClaudeSessions"; import { RunningClaudeSessions } from "@/components/RunningClaudeSessions";
@@ -28,6 +27,7 @@ import { useTabState } from "@/hooks/useTabState";
import { AnalyticsConsentBanner } from "@/components/AnalyticsConsent"; import { AnalyticsConsentBanner } from "@/components/AnalyticsConsent";
import { useAppLifecycle, useTrackEvent } from "@/hooks"; import { useAppLifecycle, useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation"; import { useTranslation } from "@/hooks/useTranslation";
import { WelcomePage } from "@/components/WelcomePage";
type View = type View =
| "welcome" | "welcome"
@@ -50,7 +50,7 @@ type View =
*/ */
function AppContent() { function AppContent() {
const { t } = useTranslation(); const { t } = useTranslation();
const [view, setView] = useState<View>("tabs"); const [view, setView] = useState<View>("welcome");
const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState(); const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState();
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [selectedProject, setSelectedProject] = useState<Project | null>(null); 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 * Loads all projects from the ~/.claude/projects directory
*/ */
@@ -219,9 +231,9 @@ function AppContent() {
/** /**
* Handles view changes with navigation protection * 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 // 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) { switch (view) {
case "welcome": case "welcome":
return ( return (
<div className="flex items-center justify-center p-4" style={{ height: "100%" }}> <WelcomePage
<div className="w-full max-w-4xl"> onNavigate={handleViewChange}
{/* Welcome Header */} onNewSession={handleNewSession}
<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>
); );
case "cc-agents": case "cc-agents":
@@ -325,19 +287,20 @@ function AppContent() {
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="mb-6" className="mb-6"
> >
<Button <div className="flex items-center gap-3 mb-4">
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => handleViewChange("welcome")} size="sm"
className="mb-4" onClick={() => handleViewChange("welcome")}
> >
{t('backToHome')} <ArrowLeft className="h-4 w-4" />
</Button> </Button>
<div className="mb-4"> <div>
<h1 className="text-3xl font-bold tracking-tight">{t('ccProjects')}</h1> <h1 className="text-3xl font-bold tracking-tight">{t('ccProjects')}</h1>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
{t('browseClaudeCodeSessions')} {t('browseClaudeCodeSessions')}
</p> </p>
</div>
</div> </div>
</motion.div> </motion.div>
@@ -375,6 +338,19 @@ function AppContent() {
projectPath={selectedProject.path} projectPath={selectedProject.path}
onBack={handleBack} onBack={handleBack}
onEditClaudeFile={handleEditClaudeFile} 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> </motion.div>
) : ( ) : (
@@ -480,19 +456,19 @@ function AppContent() {
<div className="h-screen bg-background flex flex-col"> <div className="h-screen bg-background flex flex-col">
{/* Topbar */} {/* Topbar */}
<Topbar <Topbar
onClaudeClick={() => createClaudeMdTab()} onClaudeClick={() => view === 'tabs' ? createClaudeMdTab() : handleViewChange('editor')}
onSettingsClick={() => createSettingsTab()} onSettingsClick={() => view === 'tabs' ? createSettingsTab() : handleViewChange('settings')}
onUsageClick={() => createUsageTab()} onUsageClick={() => view === 'tabs' ? createUsageTab() : handleViewChange('usage-dashboard')}
onMCPClick={() => createMCPTab()} onMCPClick={() => view === 'tabs' ? createMCPTab() : handleViewChange('mcp')}
onInfoClick={() => setShowNFO(true)} onInfoClick={() => setShowNFO(true)}
onAgentsClick={() => setShowAgentsModal(true)} onAgentsClick={() => view === 'tabs' ? setShowAgentsModal(true) : handleViewChange('cc-agents')}
/> />
{/* Analytics Consent Banner */} {/* Analytics Consent Banner */}
<AnalyticsConsentBanner /> <AnalyticsConsentBanner />
{/* Main Content */} {/* Main Content */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden relative">
{renderContent()} {renderContent()}
</div> </div>

View File

@@ -230,24 +230,24 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
<div className="flex gap-2 mb-4 pt-4"> <div className="flex gap-2 mb-4 pt-4">
<Button onClick={handleCreateAgent} className="flex-1"> <Button onClick={handleCreateAgent} className="flex-1">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Create Agent {t('agents.createAgent')}
</Button> </Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" className="flex-1"> <Button variant="outline" className="flex-1">
<Import className="w-4 h-4 mr-2" /> <Import className="w-4 h-4 mr-2" />
Import Agent {t('agents.import')}
<ChevronDown className="w-4 h-4 ml-2" /> <ChevronDown className="w-4 h-4 ml-2" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={handleImportFromFile}> <DropdownMenuItem onClick={handleImportFromFile}>
<FileJson className="w-4 h-4 mr-2" /> <FileJson className="w-4 h-4 mr-2" />
From File {t('agents.importFromFile')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={handleImportFromGitHub}> <DropdownMenuItem onClick={handleImportFromGitHub}>
<Globe className="w-4 h-4 mr-2" /> <Globe className="w-4 h-4 mr-2" />
From GitHub {t('agents.importFromGitHub')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -268,7 +268,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
window.dispatchEvent(new CustomEvent('open-create-agent-tab')); window.dispatchEvent(new CustomEvent('open-create-agent-tab'));
}}> }}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Create Agent {t('agents.createAgent')}
</Button> </Button>
</div> </div>
) : ( ) : (
@@ -299,7 +299,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
onClick={() => handleExportAgent(agent)} onClick={() => handleExportAgent(agent)}
> >
<Download className="w-3 h-3 mr-1" /> <Download className="w-3 h-3 mr-1" />
Export {t('agents.export')}
</Button> </Button>
<Button <Button
size="sm" size="sm"
@@ -308,14 +308,14 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
className="text-destructive hover:text-destructive" className="text-destructive hover:text-destructive"
> >
<Trash2 className="w-3 h-3 mr-1" /> <Trash2 className="w-3 h-3 mr-1" />
Delete {t('app.delete')}
</Button> </Button>
<Button <Button
size="sm" size="sm"
onClick={() => handleRunAgent(agent)} onClick={() => handleRunAgent(agent)}
> >
<Play className="w-3 h-3 mr-1" /> <Play className="w-3 h-3 mr-1" />
Run {t('agents.runAgent')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -331,9 +331,9 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
{runningAgents.length === 0 ? ( {runningAgents.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center"> <div className="flex flex-col items-center justify-center h-full text-center">
<Clock className="w-12 h-12 text-muted-foreground mb-4" /> <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"> <p className="text-sm text-muted-foreground">
Agent executions will appear here when started {t('agents.agentExecutionsWillAppear')}
</p> </p>
</div> </div>
) : ( ) : (
@@ -373,7 +373,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
handleOpenAgentRun(run); handleOpenAgentRun(run);
}} }}
> >
View {t('agents.view')}
</Button> </Button>
</div> </div>
</motion.div> </motion.div>
@@ -392,9 +392,9 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Delete Agent</DialogTitle> <DialogTitle>{t('agents.deleteAgentTitle')}</DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to delete "{agentToDelete?.name}"? This action cannot be undone. {t('agents.deleteConfirmation', { name: agentToDelete?.name })}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex justify-end gap-3 mt-4"> <div className="flex justify-end gap-3 mt-4">
@@ -405,13 +405,13 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
setAgentToDelete(null); setAgentToDelete(null);
}} }}
> >
Cancel {t('app.cancel')}
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
onClick={confirmDelete} onClick={confirmDelete}
> >
Delete {t('app.delete')}
</Button> </Button>
</div> </div>
</DialogContent> </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"> <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" /> <BarChart3 className="h-6 w-6 text-purple-600 dark:text-purple-400" />
</div> </div>
<DialogTitle className="text-2xl">{t('analytics.helpImproveClaudia')}</DialogTitle> <DialogTitle className="text-2xl">{t('settings.analytics.helpImproveClaudia')}</DialogTitle>
</div> </div>
<DialogDescription className="text-base mt-2"> <DialogDescription className="text-base mt-2">
{t('analytics.collectAnonymousData')} {t('settings.analytics.collectAnonymousData')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
</div> </div>
@@ -86,12 +86,12 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
<div className="flex gap-3"> <div className="flex gap-3">
<Check className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" /> <Check className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
<div className="space-y-1"> <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"> <ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
<li> {t('analytics.featureUsageDesc')}</li> <li> {t('settings.analytics.featureUsageDesc')}</li>
<li> {t('analytics.performanceMetricsDesc')}</li> <li> {t('settings.analytics.performanceMetricsDesc')}</li>
<li> {t('analytics.errorReportsDesc')}</li> <li> {t('settings.analytics.errorReportsDesc')}</li>
<li> {t('analytics.usagePatternsDesc')}</li> <li> {t('settings.analytics.usagePatternsDesc')}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -101,13 +101,13 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
<div className="flex gap-3"> <div className="flex gap-3">
<Shield className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" /> <Shield className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="space-y-1"> <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"> <ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
<li> {t('analytics.noPersonalInfo')}</li> <li> {t('settings.analytics.noPersonalInfo')}</li>
<li> {t('analytics.noFileContents')}</li> <li> {t('settings.analytics.noFileContents')}</li>
<li> {t('analytics.noApiKeys')}</li> <li> {t('settings.analytics.noApiKeys')}</li>
<li> {t('analytics.anonymousData')}</li> <li> {t('settings.analytics.anonymousData')}</li>
<li> {t('analytics.canOptOut')}</li> <li> {t('settings.analytics.canOptOut')}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -118,7 +118,7 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
<div className="flex gap-2 items-start"> <div className="flex gap-2 items-start">
<Info className="h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5" /> <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"> <p className="text-sm text-gray-600 dark:text-gray-400">
{t('analytics.dataHelpsUs')} {t('settings.analytics.dataHelpsUs')}
</p> </p>
</div> </div>
</div> </div>
@@ -130,13 +130,13 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
variant="outline" variant="outline"
className="flex-1" className="flex-1"
> >
{t('analytics.noThanks')} {t('settings.analytics.noThanks')}
</Button> </Button>
<Button <Button
onClick={handleAccept} onClick={handleAccept}
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white" className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
> >
{t('analytics.allowAnalytics')} {t('settings.analytics.allowAnalytics')}
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>
@@ -151,6 +151,7 @@ interface AnalyticsConsentBannerProps {
export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
className, className,
}) => { }) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [hasChecked, setHasChecked] = useState(false); const [hasChecked, setHasChecked] = useState(false);
@@ -199,9 +200,9 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
<div className="flex items-start gap-3"> <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" /> <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"> <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"> <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> </p>
<div className="flex gap-2 pt-1"> <div className="flex gap-2 pt-1">
<Button <Button
@@ -210,14 +211,14 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
onClick={handleDecline} onClick={handleDecline}
className="text-xs" className="text-xs"
> >
No Thanks {t('settings.analytics.noThanks')}
</Button> </Button>
<Button <Button
size="sm" size="sm"
onClick={handleAccept} onClick={handleAccept}
className="text-xs bg-purple-600 hover:bg-purple-700 text-white" className="text-xs bg-purple-600 hover:bg-purple-700 text-white"
> >
Allow {t('settings.analytics.allowAnalytics')}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -134,8 +134,8 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
return ( return (
<Card className={className}> <Card className={className}>
<CardHeader> <CardHeader>
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle> <CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.loadingAvailableInstallations')}</CardDescription> <CardDescription>{t('settings.generalOptions.loadingAvailableInstallations')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center justify-center py-4"> <div className="flex items-center justify-center py-4">
@@ -150,8 +150,8 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
return ( return (
<Card className={className}> <Card className={className}>
<CardHeader> <CardHeader>
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle> <CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.errorLoadingInstallations')}</CardDescription> <CardDescription>{t('settings.generalOptions.errorLoadingInstallations')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-sm text-destructive mb-4">{error}</div> <div className="text-sm text-destructive mb-4">{error}</div>
@@ -171,19 +171,19 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<CheckCircle className="h-5 w-5" /> <CheckCircle className="h-5 w-5" />
{t('settings.claudeCodeInstallation')} {t('settings.generalOptions.claudeCodeInstallation')}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
{t('settings.choosePreferredInstallation')} {t('settings.generalOptions.choosePreferredInstallation')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Available Installations */} {/* Available Installations */}
<div className="space-y-3"> <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}> <Select value={selectedInstallation?.path || ""} onValueChange={handleInstallationChange}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select Claude installation"> <SelectValue placeholder={t('pleaseSelectInstallation')}>
{selectedInstallation && ( {selectedInstallation && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getInstallationIcon(selectedInstallation)} {getInstallationIcon(selectedInstallation)}
@@ -198,7 +198,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
<SelectContent> <SelectContent>
{systemInstallations.length > 0 && ( {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) => ( {systemInstallations.map((installation) => (
<SelectItem key={installation.path} value={installation.path}> <SelectItem key={installation.path} value={installation.path}>
<div className="flex items-center gap-2 w-full"> <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="flex-1 min-w-0">
<div className="font-medium truncate">{installation.path}</div> <div className="font-medium truncate">{installation.path}</div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{installation.version || "Version unknown"} {installation.source} {installation.version || t('settings.versionUnknown')} {installation.source}
</div> </div>
</div> </div>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
System {t('settings.system')}
</Badge> </Badge>
</div> </div>
</SelectItem> </SelectItem>
@@ -220,7 +220,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
{customInstallations.length > 0 && ( {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) => ( {customInstallations.map((installation) => (
<SelectItem key={installation.path} value={installation.path}> <SelectItem key={installation.path} value={installation.path}>
<div className="flex items-center gap-2 w-full"> <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="flex-1 min-w-0">
<div className="font-medium truncate">{installation.path}</div> <div className="font-medium truncate">{installation.path}</div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{installation.version || "Version unknown"} {installation.source} {installation.version || t('settings.versionUnknown')} {installation.source}
</div> </div>
</div> </div>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
Custom {t('settings.custom')}
</Badge> </Badge>
</div> </div>
</SelectItem> </SelectItem>
@@ -247,16 +247,16 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
{selectedInstallation && ( {selectedInstallation && (
<div className="p-3 bg-muted rounded-lg space-y-2"> <div className="p-3 bg-muted rounded-lg space-y-2">
<div className="flex items-center justify-between"> <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))}> <Badge className={cn("text-xs", getInstallationTypeColor(selectedInstallation))}>
{selectedInstallation.installation_type} {selectedInstallation.installation_type}
</Badge> </Badge>
</div> </div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
<div><strong>Path:</strong> {selectedInstallation.path}</div> <div><strong>{t('settings.path')}:</strong> {selectedInstallation.path}</div>
<div><strong>Source:</strong> {selectedInstallation.source}</div> <div><strong>{t('settings.source')}:</strong> {selectedInstallation.source}</div>
{selectedInstallation.version && ( {selectedInstallation.version && (
<div><strong>Version:</strong> {selectedInstallation.version}</div> <div><strong>{t('settings.version')}:</strong> {selectedInstallation.version}</div>
)} )}
</div> </div>
</div> </div>
@@ -269,7 +269,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
disabled={isSaving || !selectedInstallation} disabled={isSaving || !selectedInstallation}
className="w-full" className="w-full"
> >
{isSaving ? "Saving..." : "Save Selection"} {isSaving ? t('saving') : t('saveSelection')}
</Button> </Button>
)} )}
</CardContent> </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; 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: { PreToolUse: {
label: 'Pre Tool Use', label: t('hooks.preToolUse'),
description: 'Runs before tool calls, can block and provide feedback', description: t('hooks.runsBeforeToolCalls'),
icon: <Shield className="h-4 w-4" /> icon: <Shield className="h-4 w-4" />
}, },
PostToolUse: { PostToolUse: {
label: 'Post Tool Use', label: t('hooks.postToolUse'),
description: 'Runs after successful tool completion', description: t('hooks.runsAfterToolCompletion'),
icon: <PlayCircle className="h-4 w-4" /> icon: <PlayCircle className="h-4 w-4" />
}, },
Notification: { Notification: {
label: 'Notification', label: t('hooks.notification'),
description: 'Customizes notifications when Claude needs attention', description: t('hooks.customizesNotifications'),
icon: <Zap className="h-4 w-4" /> icon: <Zap className="h-4 w-4" />
}, },
Stop: { Stop: {
label: 'Stop', label: t('hooks.stop'),
description: 'Runs when Claude finishes responding', description: t('hooks.runsWhenClaudeFinishes'),
icon: <Code2 className="h-4 w-4" /> icon: <Code2 className="h-4 w-4" />
}, },
SubagentStop: { SubagentStop: {
label: 'Subagent Stop', label: t('hooks.subagentStop'),
description: 'Runs when a Claude subagent (Task) finishes', description: t('hooks.runsWhenSubagentFinishes'),
icon: <Terminal className="h-4 w-4" /> icon: <Terminal className="h-4 w-4" />
} }
}; });
export const HooksEditor: React.FC<HooksEditorProps> = ({ export const HooksEditor: React.FC<HooksEditorProps> = ({
projectPath, projectPath,
@@ -118,6 +118,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
hideActions = false hideActions = false
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const EVENT_INFO = getEventInfo(t);
const [selectedEvent, setSelectedEvent] = useState<HookEvent>('PreToolUse'); const [selectedEvent, setSelectedEvent] = useState<HookEvent>('PreToolUse');
const [showTemplateDialog, setShowTemplateDialog] = useState(false); const [showTemplateDialog, setShowTemplateDialog] = useState(false);
const [validationErrors, setValidationErrors] = useState<string[]>([]); 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-1 space-y-2">
<div className="flex items-center gap-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> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Info className="h-3 w-3 text-muted-foreground" /> <Info className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Tool name pattern (regex supported). Leave empty to match all tools.</p> <p>{t('hooks.toolNamePatternTooltip')}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -558,7 +559,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
disabled={readOnly} disabled={readOnly}
> >
<SelectTrigger className="w-40"> <SelectTrigger className="w-40">
<SelectValue placeholder="Common patterns" /> <SelectValue placeholder={t('hooks.commonPatterns')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="custom">Custom</SelectItem> <SelectItem value="custom">Custom</SelectItem>
@@ -591,7 +592,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
> >
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label>Commands</Label> <Label>{t('hooks.commands')}</Label>
{!readOnly && ( {!readOnly && (
<Button <Button
variant="outline" variant="outline"
@@ -599,7 +600,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
onClick={() => addCommand(event, matcher.id)} onClick={() => addCommand(event, matcher.id)}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Add Command {t('hooks.addCommand')}
</Button> </Button>
)} )}
</div> </div>
@@ -613,7 +614,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<Textarea <Textarea
placeholder="Enter shell command..." placeholder={t('hooks.enterShellCommand')}
value={hook.command || ''} value={hook.command || ''}
onChange={(e) => updateCommand(event, matcher.id, hook.id, { command: e.target.value })} onChange={(e) => updateCommand(event, matcher.id, hook.id, { command: e.target.value })}
disabled={readOnly} disabled={readOnly}
@@ -633,7 +634,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
disabled={readOnly} disabled={readOnly}
className="w-20 h-8" 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> </div>
{!readOnly && ( {!readOnly && (
@@ -679,7 +680,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<Textarea <Textarea
placeholder="Enter shell command..." placeholder={t('hooks.enterShellCommand')}
value={command.command || ''} value={command.command || ''}
onChange={(e) => updateDirectCommand(event, command.id, { command: e.target.value })} onChange={(e) => updateDirectCommand(event, command.id, { command: e.target.value })}
disabled={readOnly} disabled={readOnly}
@@ -738,7 +739,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" /> <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> </div>
)} )}
@@ -759,7 +760,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<h3 className="text-lg font-semibold">{t('hooks.hooksConfiguration')}</h3> <h3 className="text-lg font-semibold">{t('hooks.hooksConfiguration')}</h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant={scope === 'project' ? 'secondary' : scope === 'local' ? 'outline' : 'default'}> <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> </Badge>
{!readOnly && ( {!readOnly && (
<> <>
@@ -769,7 +770,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
onClick={() => setShowTemplateDialog(true)} onClick={() => setShowTemplateDialog(true)}
> >
<FileText className="h-4 w-4 mr-2" /> <FileText className="h-4 w-4 mr-2" />
Templates {t('hooks.templates')}
</Button> </Button>
{!hideActions && ( {!hideActions && (
<Button <Button
@@ -783,7 +784,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
) : ( ) : (
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
)} )}
{isSaving ? "Saving..." : "Save"} {isSaving ? t('hooks.saving') : t('hooks.save')}
</Button> </Button>
)} )}
</> </>
@@ -804,7 +805,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{/* Validation Messages */} {/* Validation Messages */}
{validationErrors.length > 0 && ( {validationErrors.length > 0 && (
<div className="p-3 bg-red-500/10 rounded-md space-y-1"> <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) => ( {validationErrors.map((error, i) => (
<p key={i} className="text-xs text-red-600"> {error}</p> <p key={i} className="text-xs text-red-600"> {error}</p>
))} ))}
@@ -813,7 +814,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{validationWarnings.length > 0 && ( {validationWarnings.length > 0 && (
<div className="p-3 bg-yellow-500/10 rounded-md space-y-1"> <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) => ( {validationWarnings.map((warning, i) => (
<p key={i} className="text-xs text-yellow-600"> {warning}</p> <p key={i} className="text-xs text-yellow-600"> {warning}</p>
))} ))}
@@ -859,11 +860,11 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
{items.length === 0 ? ( {items.length === 0 ? (
<Card className="p-8 text-center"> <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 && ( {!readOnly && (
<Button onClick={() => isMatcherEvent ? addMatcher(event) : addDirectCommand(event)}> <Button onClick={() => isMatcherEvent ? addMatcher(event) : addDirectCommand(event)}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Hook {t('hooks.addHook')}
</Button> </Button>
)} )}
</Card> </Card>
@@ -881,7 +882,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
className="w-full" className="w-full"
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Another {isMatcherEvent ? 'Matcher' : 'Command'} {isMatcherEvent ? t('hooks.addAnotherMatcher') : t('hooks.addAnotherCommand')}
</Button> </Button>
)} )}
</div> </div>
@@ -895,9 +896,9 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}> <Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Hook Templates</DialogTitle> <DialogTitle>{t('hooks.hookTemplates')}</DialogTitle>
<DialogDescription> <DialogDescription>
Choose a pre-configured hook template to get started quickly {t('hooks.quickStartTemplates')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -916,7 +917,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
<p className="text-sm text-muted-foreground">{template.description}</p> <p className="text-sm text-muted-foreground">{template.description}</p>
{matcherEvents.includes(template.event as any) && template.matcher && ( {matcherEvents.includes(template.event as any) && template.matcher && (
<p className="text-xs font-mono bg-muted px-2 py-1 rounded inline-block"> <p className="text-xs font-mono bg-muted px-2 py-1 rounded inline-block">
Matcher: {template.matcher} {t('hooks.pattern')}: {template.matcher}
</p> </p>
)} )}
</div> </div>

View File

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

View File

@@ -108,7 +108,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
} }
if (!stdioCommand.trim()) { if (!stdioCommand.trim()) {
onError(t('commandRequired')); onError(t('mcp.commandRequired'));
return; return;
} }
@@ -154,7 +154,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onError(result.message); onError(result.message);
} }
} catch (error) { } catch (error) {
onError(t('failedToAddServer')); onError(t('mcp.failedToAddServer'));
console.error("Failed to add stdio server:", error); console.error("Failed to add stdio server:", error);
} finally { } finally {
setSaving(false); setSaving(false);
@@ -171,7 +171,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
} }
if (!sseUrl.trim()) { if (!sseUrl.trim()) {
onError(t('urlRequired')); onError(t('mcp.urlRequired'));
return; return;
} }
@@ -213,7 +213,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onError(result.message); onError(result.message);
} }
} catch (error) { } catch (error) {
onError(t('failedToAddServer')); onError(t('mcp.failedToAddServer'));
console.error("Failed to add SSE server:", error); console.error("Failed to add SSE server:", error);
} finally { } finally {
setSaving(false); setSaving(false);
@@ -227,7 +227,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <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 <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -235,7 +235,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="gap-2" className="gap-2"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
{t('addVariable')} {t('mcp.addVariable')}
</Button> </Button>
</div> </div>
@@ -244,14 +244,14 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{envVars.map((envVar) => ( {envVars.map((envVar) => (
<div key={envVar.id} className="flex items-center gap-2"> <div key={envVar.id} className="flex items-center gap-2">
<Input <Input
placeholder="KEY" placeholder={t('settings.placeholders.envVarKey')}
value={envVar.key} value={envVar.key}
onChange={(e) => updateEnvVar(type, envVar.id, "key", e.target.value)} onChange={(e) => updateEnvVar(type, envVar.id, "key", e.target.value)}
className="flex-1 font-mono text-sm" className="flex-1 font-mono text-sm"
/> />
<span className="text-muted-foreground">=</span> <span className="text-muted-foreground">=</span>
<Input <Input
placeholder="value" placeholder={t('settings.placeholders.envVarValue')}
value={envVar.value} value={envVar.value}
onChange={(e) => updateEnvVar(type, envVar.id, "value", e.target.value)} onChange={(e) => updateEnvVar(type, envVar.id, "value", e.target.value)}
className="flex-1 font-mono text-sm" className="flex-1 font-mono text-sm"
@@ -275,9 +275,9 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div> <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"> <p className="text-sm text-muted-foreground mt-1">
{t('configureNewMcpServer')} {t('mcp.configureNewMcpServer')}
</p> </p>
</div> </div>
@@ -298,7 +298,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
<Card className="p-6 space-y-6"> <Card className="p-6 space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="stdio-name">{t('serverName')}</Label> <Label htmlFor="stdio-name">{t('mcp.serverName')}</Label>
<Input <Input
id="stdio-name" id="stdio-name"
placeholder="my-server" placeholder="my-server"
@@ -306,12 +306,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onChange={(e) => setStdioName(e.target.value)} onChange={(e) => setStdioName(e.target.value)}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('uniqueNameToIdentify')} {t('mcp.uniqueNameToIdentify')}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="stdio-command">{t('command')}</Label> <Label htmlFor="stdio-command">{t('mcp.command')}</Label>
<Input <Input
id="stdio-command" id="stdio-command"
placeholder="/path/to/server" placeholder="/path/to/server"
@@ -320,12 +320,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono" className="font-mono"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('commandToExecuteServer')} {t('mcp.commandToExecuteServer')}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="stdio-args">{t('argumentsOptional')}</Label> <Label htmlFor="stdio-args">{t('mcp.argumentsOptional')}</Label>
<Input <Input
id="stdio-args" id="stdio-args"
placeholder="arg1 arg2 arg3" placeholder="arg1 arg2 arg3"
@@ -334,19 +334,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono" className="font-mono"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('spaceSeparatedArgs')} {t('mcp.spaceSeparatedArgs')}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="stdio-scope">{t('scope')}</Label> <Label htmlFor="stdio-scope">{t('mcp.scope')}</Label>
<SelectComponent <SelectComponent
value={stdioScope} value={stdioScope}
onValueChange={(value: string) => setStdioScope(value)} onValueChange={(value: string) => setStdioScope(value)}
options={[ options={[
{ value: "local", label: t('localProjectOnly') }, { value: "local", label: t('mcp.localProjectOnly') },
{ value: "project", label: t('projectSharedViaMcp') }, { value: "project", label: t('mcp.projectSharedViaMcp') },
{ value: "user", label: t('userAllProjects') }, { value: "user", label: t('mcp.userAllProjects') },
]} ]}
/> />
</div> </div>
@@ -363,12 +363,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{saving ? ( {saving ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
{t('addingServer')} {t('mcp.addingServer')}
</> </>
) : ( ) : (
<> <>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
{t('addStdioServer')} {t('mcp.addStdioServer')}
</> </>
)} )}
</Button> </Button>
@@ -381,7 +381,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
<Card className="p-6 space-y-6"> <Card className="p-6 space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="sse-name">{t('serverName')}</Label> <Label htmlFor="sse-name">{t('mcp.serverName')}</Label>
<Input <Input
id="sse-name" id="sse-name"
placeholder="sse-server" placeholder="sse-server"
@@ -389,12 +389,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
onChange={(e) => setSseName(e.target.value)} onChange={(e) => setSseName(e.target.value)}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('uniqueNameToIdentify')} {t('mcp.uniqueNameToIdentify')}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="sse-url">{t('url')}</Label> <Label htmlFor="sse-url">{t('mcp.url')}</Label>
<Input <Input
id="sse-url" id="sse-url"
placeholder="https://example.com/sse-endpoint" placeholder="https://example.com/sse-endpoint"
@@ -403,19 +403,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
className="font-mono" className="font-mono"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('sseEndpointUrl')} {t('mcp.sseEndpointUrl')}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="sse-scope">{t('scope')}</Label> <Label htmlFor="sse-scope">{t('mcp.scope')}</Label>
<SelectComponent <SelectComponent
value={sseScope} value={sseScope}
onValueChange={(value: string) => setSseScope(value)} onValueChange={(value: string) => setSseScope(value)}
options={[ options={[
{ value: "local", label: t('localProjectOnly') }, { value: "local", label: t('mcp.localProjectOnly') },
{ value: "project", label: t('projectSharedViaMcp') }, { value: "project", label: t('mcp.projectSharedViaMcp') },
{ value: "user", label: t('userAllProjects') }, { value: "user", label: t('mcp.userAllProjects') },
]} ]}
/> />
</div> </div>
@@ -432,12 +432,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
{saving ? ( {saving ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
{t('addingServer')} {t('mcp.addingServer')}
</> </>
) : ( ) : (
<> <>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
{t('addSseServer')} {t('mcp.addSseServer')}
</> </>
)} )}
</Button> </Button>
@@ -451,7 +451,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2 text-sm font-medium"> <div className="flex items-center gap-2 text-sm font-medium">
<Info className="h-4 w-4 text-primary" /> <Info className="h-4 w-4 text-primary" />
<span>{t('exampleCommands')}</span> <span>{t('mcp.exampleCommands')}</span>
</div> </div>
<div className="space-y-2 text-xs text-muted-foreground"> <div className="space-y-2 text-xs text-muted-foreground">
<div className="font-mono bg-background p-2 rounded"> <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 { Label } from "@/components/ui/label";
import { SelectComponent } from "@/components/ui/select"; import { SelectComponent } from "@/components/ui/select";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { useTranslation } from "@/hooks/useTranslation";
interface MCPImportExportProps { interface MCPImportExportProps {
/** /**
@@ -24,6 +25,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
onImportCompleted, onImportCompleted,
onError, onError,
}) => { }) => {
const { t } = useTranslation();
const [importingDesktop, setImportingDesktop] = useState(false); const [importingDesktop, setImportingDesktop] = useState(false);
const [importingJson, setImportingJson] = useState(false); const [importingJson, setImportingJson] = useState(false);
const [importScope, setImportScope] = useState("local"); const [importScope, setImportScope] = useState("local");
@@ -163,9 +165,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div> <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"> <p className="text-sm text-muted-foreground mt-1">
Import MCP servers from other sources or export your configuration {t('mcp.importExportDescription')}
</p> </p>
</div> </div>
@@ -175,19 +177,19 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Settings2 className="h-4 w-4 text-slate-500" /> <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> </div>
<SelectComponent <SelectComponent
value={importScope} value={importScope}
onValueChange={(value: string) => setImportScope(value)} onValueChange={(value: string) => setImportScope(value)}
options={[ options={[
{ value: "local", label: "Local (this project only)" }, { value: "local", label: t('mcp.localProjectOnly') },
{ value: "project", label: "Project (shared via .mcp.json)" }, { value: "project", label: t('mcp.projectShared') },
{ value: "user", label: "User (all projects)" }, { value: "user", label: t('mcp.userAllProjects') },
]} ]}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Choose where to save imported servers from JSON files {t('mcp.chooseImportLocation')}
</p> </p>
</div> </div>
</Card> </Card>
@@ -200,9 +202,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<Download className="h-5 w-5 text-blue-500" /> <Download className="h-5 w-5 text-blue-500" />
</div> </div>
<div className="flex-1"> <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"> <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> </p>
</div> </div>
</div> </div>
@@ -214,12 +216,12 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
{importingDesktop ? ( {importingDesktop ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
Importing... {t('mcp.importing')}
</> </>
) : ( ) : (
<> <>
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
Import from Claude Desktop {t('mcp.importFromClaudeDesktop')}
</> </>
)} )}
</Button> </Button>
@@ -234,9 +236,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<FileText className="h-5 w-5 text-purple-500" /> <FileText className="h-5 w-5 text-purple-500" />
</div> </div>
<div className="flex-1"> <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"> <p className="text-xs text-muted-foreground mt-1">
Import server configuration from a JSON file {t('mcp.importFromJSONDescription')}
</p> </p>
</div> </div>
</div> </div>
@@ -258,12 +260,12 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
{importingJson ? ( {importingJson ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
Importing... {t('mcp.importing')}
</> </>
) : ( ) : (
<> <>
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4" />
Choose JSON File {t('mcp.chooseJSONFile')}
</> </>
)} )}
</Button> </Button>
@@ -279,9 +281,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<Upload className="h-5 w-5 text-muted-foreground" /> <Upload className="h-5 w-5 text-muted-foreground" />
</div> </div>
<div className="flex-1"> <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"> <p className="text-xs text-muted-foreground mt-1">
Export your MCP server configuration {t('mcp.exportConfigurationDescription')}
</p> </p>
</div> </div>
</div> </div>
@@ -292,7 +294,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
className="w-full gap-2" className="w-full gap-2"
> >
<Upload className="h-4 w-4" /> <Upload className="h-4 w-4" />
Export (Coming Soon) {t('mcp.exportComingSoon')}
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -305,9 +307,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<Network className="h-5 w-5 text-green-500" /> <Network className="h-5 w-5 text-green-500" />
</div> </div>
<div className="flex-1"> <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"> <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> </p>
</div> </div>
</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" 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" /> <Network className="h-4 w-4" />
Start MCP Server {t('mcp.startMCPServer')}
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -328,11 +330,11 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2 text-sm font-medium"> <div className="flex items-center gap-2 text-sm font-medium">
<Info className="h-4 w-4 text-primary" /> <Info className="h-4 w-4 text-primary" />
<span>JSON Format Examples</span> <span>{t('mcp.jsonFormatExamples')}</span>
</div> </div>
<div className="space-y-3 text-xs"> <div className="space-y-3 text-xs">
<div> <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"> <pre className="bg-background p-3 rounded-lg overflow-x-auto">
{`{ {`{
"type": "stdio", "type": "stdio",
@@ -343,7 +345,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
</pre> </pre>
</div> </div>
<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"> <pre className="bg-background p-3 rounded-lg overflow-x-auto">
{`{ {`{
"mcpServers": { "mcpServers": {

View File

@@ -155,15 +155,15 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
<TabsList className="grid w-full max-w-md grid-cols-3"> <TabsList className="grid w-full max-w-md grid-cols-3">
<TabsTrigger value="servers" className="gap-2"> <TabsTrigger value="servers" className="gap-2">
<Network className="h-4 w-4 text-blue-500" /> <Network className="h-4 w-4 text-blue-500" />
{t('servers')} {t('mcp.servers')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="add" className="gap-2"> <TabsTrigger value="add" className="gap-2">
<Plus className="h-4 w-4 text-green-500" /> <Plus className="h-4 w-4 text-green-500" />
{t('addServer')} {t('mcp.addServer')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="import" className="gap-2"> <TabsTrigger value="import" className="gap-2">
<Download className="h-4 w-4 text-purple-500" /> <Download className="h-4 w-4 text-purple-500" />
{t('importExport')} {t('mcp.importExport')}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>

View File

@@ -186,11 +186,11 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
const getScopeDisplayName = (scope: string) => { const getScopeDisplayName = (scope: string) => {
switch (scope) { switch (scope) {
case "local": case "local":
return t('localProjectSpecific'); return t('mcp.localProjectSpecific');
case "project": case "project":
return t('projectSharedMcp'); return t('mcp.projectSharedMcp');
case "user": case "user":
return t('userAllProjects'); return t('mcp.userAllProjects');
default: default:
return scope; return scope;
} }
@@ -222,7 +222,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.status?.running && ( {server.status?.running && (
<Badge variant="outline" className="gap-1 flex-shrink-0 border-green-500/50 text-green-600 bg-green-500/10"> <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" /> <CheckCircle className="h-3 w-3" />
{t('running')} {t('mcp.running')}
</Badge> </Badge>
)} )}
</div> </div>
@@ -239,7 +239,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
className="h-6 px-2 text-xs hover:bg-primary/10" className="h-6 px-2 text-xs hover:bg-primary/10"
> >
<ChevronDown className="h-3 w-3 mr-1" /> <ChevronDown className="h-3 w-3 mr-1" />
{t('showFull')} {t('mcp.showFull')}
</Button> </Button>
</div> </div>
)} )}
@@ -254,7 +254,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{Object.keys(server.env).length > 0 && !isExpanded && ( {Object.keys(server.env).length > 0 && !isExpanded && (
<div className="flex items-center gap-1 text-xs text-muted-foreground pl-9"> <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>
)} )}
</div> </div>
@@ -301,7 +301,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.command && ( {server.command && (
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center justify-between"> <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"> <div className="flex items-center gap-1">
<Button <Button
variant="ghost" variant="ghost"
@@ -310,7 +310,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
className="h-6 px-2 text-xs hover:bg-primary/10" className="h-6 px-2 text-xs hover:bg-primary/10"
> >
<Copy className="h-3 w-3 mr-1" /> <Copy className="h-3 w-3 mr-1" />
{isCopied ? t('copied') : t('copy')} {isCopied ? t('mcp.copied') : t('mcp.copy')}
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@@ -319,7 +319,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
className="h-6 px-2 text-xs hover:bg-primary/10" className="h-6 px-2 text-xs hover:bg-primary/10"
> >
<ChevronUp className="h-3 w-3 mr-1" /> <ChevronUp className="h-3 w-3 mr-1" />
{t('hide')} {t('mcp.hide')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -331,7 +331,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.args && server.args.length > 0 && ( {server.args && server.args.length > 0 && (
<div className="space-y-1"> <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"> <div className="text-xs font-mono bg-muted/50 p-2 rounded space-y-1">
{server.args.map((arg, idx) => ( {server.args.map((arg, idx) => (
<div key={idx} className="break-all"> <div key={idx} className="break-all">
@@ -345,7 +345,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{server.transport === "sse" && server.url && ( {server.transport === "sse" && server.url && (
<div className="space-y-1"> <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"> <p className="text-xs font-mono bg-muted/50 p-2 rounded break-all">
{server.url} {server.url}
</p> </p>
@@ -354,7 +354,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{Object.keys(server.env).length > 0 && ( {Object.keys(server.env).length > 0 && (
<div className="space-y-1"> <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"> <div className="text-xs font-mono bg-muted/50 p-2 rounded space-y-1">
{Object.entries(server.env).map(([key, value]) => ( {Object.entries(server.env).map(([key, value]) => (
<div key={key} className="break-all"> <div key={key} className="break-all">
@@ -386,9 +386,9 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div> <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"> <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> </p>
</div> </div>
<Button <Button

View File

@@ -53,7 +53,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
setOriginalContent(prompt); setOriginalContent(prompt);
} catch (err) { } catch (err) {
console.error("Failed to load system prompt:", err); console.error("Failed to load system prompt:", err);
setError(t('loadClaudemdFailed')); setError(t('usage.loadClaudemdFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -66,11 +66,11 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
setToast(null); setToast(null);
await api.saveSystemPrompt(content); await api.saveSystemPrompt(content);
setOriginalContent(content); setOriginalContent(content);
setToast({ message: t('claudemdSavedSuccess'), type: "success" }); setToast({ message: t('usage.claudemdSavedSuccess'), type: "success" });
} catch (err) { } catch (err) {
console.error("Failed to save system prompt:", err); console.error("Failed to save system prompt:", err);
setError(t('saveClaudemdFailed')); setError(t('usage.saveClaudemdFailed'));
setToast({ message: t('saveClaudemdFailed'), type: "error" }); setToast({ message: t('usage.saveClaudemdFailed'), type: "error" });
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -79,7 +79,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
const handleBack = () => { const handleBack = () => {
if (hasChanges) { if (hasChanges) {
const confirmLeave = window.confirm( const confirmLeave = window.confirm(
t('unsavedChangesConfirm') t('usage.unsavedChangesConfirm')
); );
if (!confirmLeave) return; if (!confirmLeave) return;
} }
@@ -108,7 +108,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
<div> <div>
<h2 className="text-lg font-semibold">CLAUDE.md</h2> <h2 className="text-lg font-semibold">CLAUDE.md</h2>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('editSystemPrompt')} {t('usage.editSystemPrompt')}
</p> </p>
</div> </div>
</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 justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={onBack}> <Button variant="ghost" size="sm" onClick={onBack}>
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4" />
Back
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" /> <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 = () => { export const TabContent: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { tabs, activeTabId, createChatTab, findTabBySessionId, createClaudeFileTab, createAgentExecutionTab, createCreateAgentTab, createImportAgentTab, closeTab, updateTab } = useTabState(); 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 // Listen for events to open sessions in tabs
useEffect(() => { useEffect(() => {

View File

@@ -142,6 +142,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
createProjectsTab, createProjectsTab,
closeTab, closeTab,
switchToTab, switchToTab,
updateTab,
canAddTab canAddTab
} = useTabState(); } = 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('create-chat-tab', handleCreateTab);
window.addEventListener('close-current-tab', handleCloseTab); window.addEventListener('close-current-tab', handleCloseTab);
window.addEventListener('switch-to-next-tab', handleNextTab); window.addEventListener('switch-to-next-tab', handleNextTab);
window.addEventListener('switch-to-previous-tab', handlePreviousTab); window.addEventListener('switch-to-previous-tab', handlePreviousTab);
window.addEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener); window.addEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);
window.addEventListener('open-session-tab', handleOpenSessionTab as EventListener);
return () => { return () => {
window.removeEventListener('create-chat-tab', handleCreateTab); 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-next-tab', handleNextTab);
window.removeEventListener('switch-to-previous-tab', handlePreviousTab); window.removeEventListener('switch-to-previous-tab', handlePreviousTab);
window.removeEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener); 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 // Check scroll buttons visibility
const checkScrollButtons = () => { const checkScrollButtons = () => {
@@ -319,7 +340,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
className={cn( className={cn(
"p-1.5 hover:bg-muted/80 rounded-sm z-20 ml-1", "p-1.5 hover:bg-muted/80 rounded-sm z-20 ml-1",
"transition-colors duration-200 flex items-center justify-center", "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" title="Scroll tabs left"
> >
@@ -373,7 +394,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
className={cn( className={cn(
"p-1.5 hover:bg-muted/80 rounded-sm z-20 mr-1", "p-1.5 hover:bg-muted/80 rounded-sm z-20 mr-1",
"transition-colors duration-200 flex items-center justify-center", "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" title="Scroll tabs right"
> >
@@ -390,7 +411,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
disabled={!canAddTab()} disabled={!canAddTab()}
className={cn( className={cn(
"p-2 mx-2 rounded-md transition-all duration-200 flex items-center justify-center", "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() canAddTab()
? "hover:bg-muted/80 hover:border-border text-muted-foreground hover:text-foreground hover:shadow-sm" ? "hover:bg-muted/80 hover:border-border text-muted-foreground hover:text-foreground hover:shadow-sm"
: "opacity-50 cursor-not-allowed bg-muted/30" : "opacity-50 cursor-not-allowed bg-muted/30"

View File

@@ -107,7 +107,11 @@ export const Topbar: React.FC<TopbarProps> = ({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-auto py-1 px-2 hover:bg-accent" 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"> <div className="flex items-center space-x-2 text-xs">
<Circle <Circle
@@ -172,7 +176,7 @@ export const Topbar: React.FC<TopbarProps> = ({
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className={cn( 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 className
)} )}
> >

View File

@@ -16,6 +16,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
interface WebviewPreviewProps { interface WebviewPreviewProps {
/** /**
@@ -61,6 +62,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
onUrlChange, onUrlChange,
className, className,
}) => { }) => {
const { t } = useTranslation();
const [currentUrl, setCurrentUrl] = useState(initialUrl); const [currentUrl, setCurrentUrl] = useState(initialUrl);
const [inputUrl, setInputUrl] = useState(initialUrl); const [inputUrl, setInputUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -132,7 +134,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
onUrlChange?.(finalUrl); onUrlChange?.(finalUrl);
} catch (err) { } catch (err) {
setHasError(true); 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 justify-between px-3 py-2 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" /> <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 && ( {isLoading && (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" /> <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
)} )}
@@ -207,7 +209,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{isMaximized ? "Exit full screen (ESC)" : "Enter full screen"} {isMaximized ? t('webview.exitFullScreen') : t('webview.enterFullScreen')}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -270,7 +272,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
value={inputUrl} value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)} onChange={(e) => setInputUrl(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Enter URL..." placeholder={t('webview.enterUrl')}
className="pr-10 h-8 text-sm font-mono" className="pr-10 h-8 text-sm font-mono"
/> />
{inputUrl !== currentUrl && ( {inputUrl !== currentUrl && (
@@ -300,7 +302,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
> >
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-primary" /> <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> </div>
</motion.div> </motion.div>
)} )}
@@ -310,12 +312,12 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
{hasError ? ( {hasError ? (
<div className="flex flex-col items-center justify-center h-full p-8"> <div className="flex flex-col items-center justify-center h-full p-8">
<AlertCircle className="h-12 w-12 text-destructive mb-4" /> <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"> <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> </p>
<Button onClick={handleRefresh} variant="outline" size="sm"> <Button onClick={handleRefresh} variant="outline" size="sm">
Try Again {t('app.retry')}
</Button> </Button>
</div> </div>
) : currentUrl ? ( ) : currentUrl ? (
@@ -336,14 +338,14 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
// Empty state when no URL is provided // Empty state when no URL is provided
<div className="flex flex-col items-center justify-center h-full p-8 text-foreground"> <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" /> <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"> <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> </p>
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <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> <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>
</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 i18n from 'i18next';
import { initReactI18next } from 'react-i18next'; import {initReactI18next} from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
// 引入语言资源文件 // 引入语言资源文件
@@ -8,50 +8,50 @@ import zh from '@/locales/zh/common.json';
// 配置语言检测器 // 配置语言检测器
const languageDetectorOptions = { const languageDetectorOptions = {
// 检测顺序 // 检测顺序
order: ['localStorage', 'navigator', 'htmlTag'], order: ['localStorage', 'navigator', 'htmlTag'],
// 缓存语言到localStorage // 缓存语言到localStorage
caches: ['localStorage'], caches: ['localStorage'],
// 检查所有可用语言 // 检查所有可用语言
checkWhitelist: true, checkWhitelist: true,
}; };
i18n i18n
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
// 回退语言 // 回退语言
fallbackLng: 'en', fallbackLng: 'en',
// 调试模式(开发环境) // 调试模式(开发环境)
debug: process.env.NODE_ENV === 'development', debug: process.env.NODE_ENV === 'development',
// 语言资源 // 语言资源
resources: { resources: {
en: { en: {
common: en, common: en,
}, },
zh: { zh: {
common: zh, common: zh,
}, },
}, },
// 命名空间配置 // 命名空间配置
defaultNS: 'common', defaultNS: 'common',
ns: ['common'], ns: ['common'],
// 语言检测选项 // 语言检测选项
detection: languageDetectorOptions, detection: languageDetectorOptions,
// 插值配置 // 插值配置
interpolation: { interpolation: {
escapeValue: false, // React 已经默认防止XSS escapeValue: false, // React 已经默认防止XSS
}, },
// 白名单支持的语言 // 白名单支持的语言
supportedLngs: ['en', 'zh'], supportedLngs: ['en', 'zh'],
// 非显式支持的语言回退到en // 非显式支持的语言回退到en
nonExplicitSupportedLngs: true, nonExplicitSupportedLngs: true,
}); });
export default i18n; export default i18n;

View File

@@ -1,4 +1,7 @@
{ {
"pleaseSelectInstallation": "Select Claude installation",
"saveSelection": "Save Selection",
"saving": "Saving...",
"ccProjects": "CC Projects", "ccProjects": "CC Projects",
"browseClaudeCodeSessions": "Browse your Claude Code sessions", "browseClaudeCodeSessions": "Browse your Claude Code sessions",
"newClaudeCodeSession": "New Claude Code session", "newClaudeCodeSession": "New Claude Code session",
@@ -8,6 +11,7 @@
"app": { "app": {
"name": "Claudia", "name": "Claudia",
"welcome": "Welcome to Claudia", "welcome": "Welcome to Claudia",
"tagline": "Powerful Claude Code session management tool",
"loading": "Loading...", "loading": "Loading...",
"error": "Error", "error": "Error",
"success": "Success", "success": "Success",
@@ -40,6 +44,21 @@
"mcp": "MCP Manager", "mcp": "MCP Manager",
"about": "About" "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": { "projects": {
"title": "Projects", "title": "Projects",
"noProjects": "No projects found", "noProjects": "No projects found",
@@ -58,8 +77,24 @@
"editAgent": "Edit Agent", "editAgent": "Edit Agent",
"deleteAgent": "Delete Agent", "deleteAgent": "Delete Agent",
"executeAgent": "Execute Agent", "executeAgent": "Execute Agent",
"agentName": "Agent Name",
"agentNameRequired": "Agent name is required", "agentNameRequired": "Agent name is required",
"agentIcon": "Agent Icon", "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", "systemPrompt": "System Prompt",
"systemPromptRequired": "System prompt is required", "systemPromptRequired": "System prompt is required",
"defaultTask": "Default Task", "defaultTask": "Default Task",
@@ -110,7 +145,10 @@
"noAgentsAvailable": "No agents available", "noAgentsAvailable": "No agents available",
"availableAgents": "Available Agents", "availableAgents": "Available Agents",
"runningAgents": "Running 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": {
"slashCommands": "Slash Commands", "slashCommands": "Slash Commands",
@@ -131,7 +169,46 @@
"hooksConfiguration": "Hooks Configuration", "hooksConfiguration": "Hooks Configuration",
"configureShellCommands": "Configure shell commands to execute at various points in Claude Code's lifecycle.", "configureShellCommands": "Configure shell commands to execute at various points in Claude Code's lifecycle.",
"localSettingsNote": " These settings are not committed to version control.", "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": { "settings": {
"title": "Settings", "title": "Settings",
@@ -195,13 +272,29 @@
"chatRetention": "Chat Transcript Retention (days)", "chatRetention": "Chat Transcript Retention (days)",
"chatRetentionDesc": "How long to retain chat transcripts locally (default: 30 days)", "chatRetentionDesc": "How long to retain chat transcripts locally (default: 30 days)",
"claudeCodeInstallation": "Claude Code Installation", "claudeCodeInstallation": "Claude Code Installation",
"choosePreferredInstallation": "Choose your preferred Claude Code installation.", "choosePreferredInstallation": "Choose your preferred Claude Code installation.",
"loadingAvailableInstallations": "Loading available installations...", "loadingAvailableInstallations": "Loading available installations...",
"errorLoadingInstallations": "Error loading installations", "errorLoadingInstallations": "Error loading installations",
"availableInstallations": "Available Installations", "availableInstallations": "Available Installations",
"claudeCodeInstallationDesc": "Select which Claude Code installation to use.", "claudeCodeInstallationDesc": "Select which Claude Code installation to use.",
"binaryPathChanged": "⚠️ Claude binary path has been changed. Remember to save your settings." "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": { "permissions": {
"permissionRules": "Permission Rules", "permissionRules": "Permission Rules",
"permissionRulesDesc": "Control which tools Claude Code can use without manual approval", "permissionRulesDesc": "Control which tools Claude Code can use without manual approval",
@@ -257,8 +350,8 @@
"analyticsEnabled": "Analytics enabled", "analyticsEnabled": "Analytics enabled",
"analyticsDisabled": "Analytics disabled", "analyticsDisabled": "Analytics disabled",
"allDataDeleted": "All analytics data deleted", "allDataDeleted": "All analytics data deleted",
"helpImproveClaudia": "Help Improve Claudia", "helpImproveClaudia": "Help improve Claudia",
"collectAnonymousData": "We'd like to collect anonymous usage data to improve your experience.", "collectAnonymousData": "We collect anonymous usage data to improve your experience. No personal data is collected.",
"featureUsageDesc": "Feature usage (which tools and commands you use)", "featureUsageDesc": "Feature usage (which tools and commands you use)",
"performanceMetricsDesc": "Performance metrics (app speed and reliability)", "performanceMetricsDesc": "Performance metrics (app speed and reliability)",
"errorReportsDesc": "Error reports (to fix bugs and improve stability)", "errorReportsDesc": "Error reports (to fix bugs and improve stability)",
@@ -289,6 +382,7 @@
}, },
"mcp": { "mcp": {
"title": "MCP Server Management", "title": "MCP Server Management",
"servers": "Servers",
"addServer": "Add Server", "addServer": "Add Server",
"serverName": "Server Name", "serverName": "Server Name",
"serverCommand": "Server Command", "serverCommand": "Server Command",
@@ -298,7 +392,72 @@
"connectionFailed": "Connection failed", "connectionFailed": "Connection failed",
"importFromClaude": "Import from Claude Desktop", "importFromClaude": "Import from Claude Desktop",
"exportConfig": "Export Configuration", "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": { "usage": {
"title": "Usage Dashboard", "title": "Usage Dashboard",
@@ -398,7 +557,9 @@
"addServerToGetStarted": "Add a server to get started with Model Context Protocol", "addServerToGetStarted": "Add a server to get started with Model Context Protocol",
"localProjectSpecific": "Local (Project-specific)", "localProjectSpecific": "Local (Project-specific)",
"projectSharedMcp": "Project (Shared via .mcp.json)", "projectSharedMcp": "Project (Shared via .mcp.json)",
"userAllProjects": "User (All projects)", "userAllProjects": "User (All projects)"
},
"messages": {
"welcomeToClaudia": "Welcome to Claudia", "welcomeToClaudia": "Welcome to Claudia",
"backToHome": "Back to Home", "backToHome": "Back to Home",
"noProjectsFound": "No projects found in ~/.claude/projects", "noProjectsFound": "No projects found in ~/.claude/projects",
@@ -421,7 +582,41 @@
"loadFileFailed": "Failed to load CLAUDE.md file", "loadFileFailed": "Failed to load CLAUDE.md file",
"editProjectSpecificPrompt": "Edit project-specific Claude Code system prompt", "editProjectSpecificPrompt": "Edit project-specific Claude Code system prompt",
"maximumTabsReached": "Maximum number of tabs ({{max}}) reached", "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": { "checkpoint": {
"title": "Checkpoints", "title": "Checkpoints",
@@ -477,41 +672,35 @@
"minLength": "Minimum {{count}} characters required", "minLength": "Minimum {{count}} characters required",
"maxLength": "Maximum {{count}} characters allowed" "maxLength": "Maximum {{count}} characters allowed"
}, },
"messages": { "errorBoundary": {
"saveSuccess": "Saved successfully", "somethingWentWrong": "Something went wrong",
"deleteSuccess": "Deleted successfully", "errorOccurred": "An error occurred while rendering this component.",
"operationFailed": "Operation failed", "errorDetails": "Error details",
"confirmAction": "Are you sure you want to perform this action?", "tryAgain": "Try again"
"unsavedChanges": "You have unsaved changes", },
"networkError": "Network error occurred", "webview": {
"unknownError": "Unknown error occurred", "preview": "Preview",
"claudeCodeNotFound": "Claude Code not found", "exitFullScreen": "Exit full screen (ESC)",
"selectClaudeInstallation": "Select Claude Installation", "enterFullScreen": "Enter full screen",
"installClaudeCode": "Install Claude Code", "enterUrl": "Enter URL...",
"noTabsOpen": "No tabs open", "loadingPreview": "Loading preview...",
"clickPlusToStartChat": "Click the + button to start a new chat", "failedToLoad": "Failed to load preview",
"noAgentRunIdSpecified": "No agent run ID specified", "pageCouldNotLoad": "The page could not be loaded. Please check the URL and try again.",
"noClaudeFileIdSpecified": "No Claude file ID specified", "invalidUrl": "Invalid URL",
"claudeFileEditorNotImplemented": "Claude file editor not yet implemented in tabs", "enterUrlToPreview": "Enter a URL to preview",
"noAgentDataSpecified": "No agent data specified", "enterUrlDescription": "Enter a URL in the address bar above to preview a website.",
"importAgentComingSoon": "Import agent functionality coming soon...", "tryEntering": "Try entering",
"unknownTabType": "Unknown tab type", "orAnyOtherUrl": "or any other URL",
"letClaudeDecide": "Let Claude decide", "unknownTime": "Unknown time",
"basicReasoning": "Basic reasoning", "sessions": "Sessions",
"deeperAnalysis": "Deeper analysis", "selectProjectDirectory": "Select Project Directory",
"extensiveReasoning": "Extensive reasoning", "failedToSendPrompt": "Failed to send prompt",
"maximumAnalysis": "Maximum analysis", "sessionOutputCopiedJsonl": "Session output copied as JSONL",
"typeYourPromptHere": "Type your prompt here...", "sessionOutputCopiedMarkdown": "Session output copied as Markdown",
"dropImagesHere": "Drop images here...", "failedToCopy": "Failed to copy",
"askClaudeAnything": "Ask Claude anything...", "forkSession": "Fork Session from Checkpoint",
"usage": "Usage", "newSessionName": "New Session Name",
"mcpServersTitle": "MCP Servers", "enterNameForForkedSession": "Enter a name for the forked session"
"claudemdTitle": "CLAUDE.md",
"runAgent": "Run: {{name}}",
"createAgent": "Create Agent",
"importAgent": "Import Agent",
"unsavedChangesCloseConfirm": "Tab \"{{title}}\" has unsaved changes. Close anyway?",
"session": "Session"
}, },
"input": { "input": {
"pressEnterToSend": "Press Enter to send, Shift+Enter for new line", "pressEnterToSend": "Press Enter to send, Shift+Enter for new line",

View File

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

View File

@@ -10,6 +10,21 @@
display: none; 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 */ /* Dark theme configuration */
@theme { @theme {
/* Colors */ /* Colors */
@@ -45,8 +60,8 @@
--radius-xl: 1rem; --radius-xl: 1rem;
/* Fonts */ /* Fonts */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; --font-sans: "Maple Mono", -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-mono: "Maple Mono", ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
/* Transitions */ /* Transitions */
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
@@ -56,26 +71,26 @@
/* Theme Variations */ /* Theme Variations */
/* Default is dark theme - already defined above */ /* Default is dark theme - already defined above */
/* Light Theme */ /* Light Theme - Enhanced Contrast */
.theme-light { .theme-light {
--color-background: oklch(0.98 0.01 240); --color-background: oklch(0.99 0.005 240);
--color-foreground: oklch(0.12 0.01 240); --color-foreground: oklch(0.08 0.01 240);
--color-card: oklch(0.96 0.01 240); --color-card: oklch(0.97 0.005 240);
--color-card-foreground: oklch(0.12 0.01 240); --color-card-foreground: oklch(0.08 0.01 240);
--color-popover: oklch(0.98 0.01 240); --color-popover: oklch(0.99 0.005 240);
--color-popover-foreground: oklch(0.12 0.01 240); --color-popover-foreground: oklch(0.08 0.01 240);
--color-primary: oklch(0.12 0.01 240); --color-primary: oklch(0.08 0.01 240);
--color-primary-foreground: oklch(0.98 0.01 240); --color-primary-foreground: oklch(0.99 0.005 240);
--color-secondary: oklch(0.94 0.01 240); --color-secondary: oklch(0.95 0.005 240);
--color-secondary-foreground: oklch(0.12 0.01 240); --color-secondary-foreground: oklch(0.08 0.01 240);
--color-muted: oklch(0.94 0.01 240); --color-muted: oklch(0.95 0.005 240);
--color-muted-foreground: oklch(0.45 0.01 240); --color-muted-foreground: oklch(0.45 0.01 240);
--color-accent: oklch(0.94 0.01 240); --color-accent: oklch(0.95 0.005 240);
--color-accent-foreground: oklch(0.12 0.01 240); --color-accent-foreground: oklch(0.08 0.01 240);
--color-destructive: oklch(0.6 0.2 25); --color-destructive: oklch(0.6 0.2 25);
--color-destructive-foreground: oklch(0.98 0.01 240); --color-destructive-foreground: oklch(0.99 0.005 240);
--color-border: oklch(0.90 0.01 240); --color-border: oklch(0.85 0.005 240);
--color-input: oklch(0.90 0.01 240); --color-input: oklch(0.85 0.005 240);
--color-ring: oklch(0.52 0.015 240); --color-ring: oklch(0.52 0.015 240);
/* Additional colors for status messages */ /* Additional colors for status messages */
@@ -155,6 +170,11 @@ body {
background-color: var(--color-background); background-color: var(--color-background);
color: var(--color-foreground); color: var(--color-foreground);
font-family: var(--font-sans); 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 */ /* 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 { .shimmer-hover {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -777,3 +803,33 @@ code::-webkit-scrollbar-thumb:hover {
.image-move-to-input { .image-move-to-input {
animation: moveToInput 0.8s ease-in-out forwards; 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);
}