增加永久存储记录信息

完善 i18n
This commit is contained in:
2025-10-11 14:55:34 +08:00
parent 5bae979ed6
commit 25db9ed1f3
24 changed files with 502 additions and 233 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, lazy, Suspense } from "react"; import { useState, useEffect, lazy, Suspense } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Plus, Loader2, ArrowLeft } from "lucide-react"; import { 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";
@@ -98,6 +98,13 @@ function AppContent() {
initializeBackendLanguage(); initializeBackendLanguage();
}, []); // Run once on app startup }, []); // Run once on app startup
// Update document title based on current language
useEffect(() => {
try {
document.title = `${t('app.name')} - ${t('app.tagline')}`;
} catch {}
}, [t]);
// Track when user reaches different journey stages // Track when user reaches different journey stages
useEffect(() => { useEffect(() => {
if (view === "projects" && projects.length > 0 && !hasTrackedFirstChat) { if (view === "projects" && projects.length > 0 && !hasTrackedFirstChat) {
@@ -427,32 +434,16 @@ function AppContent() {
exit={{ opacity: 0, x: 20 }} exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
{/* New session button at the top */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-4"
>
<Button
onClick={handleNewSession}
size="default"
className="w-full max-w-md"
>
<Plus className="mr-2 h-4 w-4" />
{t('newClaudeCodeSession')}
</Button>
</motion.div>
{/* Running Claude Sessions */} {/* Running Claude Sessions */}
<RunningClaudeSessions /> <RunningClaudeSessions />
{/* Project list */} {/* Project list with integrated new session button */}
{projects.length > 0 ? ( {projects.length > 0 ? (
<ProjectList <ProjectList
projects={projects} projects={projects}
onProjectClick={handleProjectClick} onProjectClick={handleProjectClick}
onProjectSettings={handleProjectSettings} onProjectSettings={handleProjectSettings}
onNewSession={handleNewSession}
loading={loading} loading={loading}
className="animate-fade-in" className="animate-fade-in"
/> />

View File

@@ -274,7 +274,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
const selected = await open({ const selected = await open({
directory: true, directory: true,
multiple: false, multiple: false,
title: "Select Project Directory" title: t('webview.selectProjectDirectory')
}); });
if (selected) { if (selected) {
@@ -576,7 +576,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<div> <div>
<h1 className="text-xl font-bold">{t('agents.execute')}: {agent.name}</h1> <h1 className="text-xl font-bold">{t('agents.execute')}: {agent.name}</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{model === 'opus' ? 'Claude 4.1 Opus' : 'Claude 4 Sonnet'} {model === 'opus' ? t('agents.opusName') : t('agents.sonnetName')}
</p> </p>
</div> </div>
</div> </div>
@@ -669,7 +669,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<div className="w-1.5 h-1.5 rounded-full bg-primary-foreground" /> <div className="w-1.5 h-1.5 rounded-full bg-primary-foreground" />
)} )}
</div> </div>
<span>Claude 4 Sonnet</span> <span>{t('agents.sonnetName')}</span>
</div> </div>
</button> </button>
@@ -695,7 +695,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<div className="w-1.5 h-1.5 rounded-full bg-primary-foreground" /> <div className="w-1.5 h-1.5 rounded-full bg-primary-foreground" />
)} )}
</div> </div>
<span>Claude 4.1 Opus</span> <span>{t('agents.opusName')}</span>
</div> </div>
</button> </button>
</div> </div>
@@ -772,7 +772,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
<span className="text-sm text-muted-foreground">Initializing agent...</span> <span className="text-sm text-muted-foreground">{t('agents.initializing') || 'Initializing agent...'}</span>
</div> </div>
</div> </div>
)} )}
@@ -826,11 +826,11 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<div className="flex items-center justify-between p-4 border-b border-border"> <div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{renderIcon()} {renderIcon()}
<h2 className="text-lg font-semibold">{agent.name} - Output</h2> <h2 className="text-lg font-semibold">{agent.name} - {t('app.output')}</h2>
{isRunning && ( {isRunning && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-xs text-green-600 font-medium">Running</span> <span className="text-xs text-green-600 font-medium">{t('agents.statusRunning')}</span>
</div> </div>
)} )}
</div> </div>
@@ -878,7 +878,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
Close {t('app.close')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -914,7 +914,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
<span className="text-sm text-muted-foreground">Initializing agent...</span> <span className="text-sm text-muted-foreground">{t('agents.initializing') || 'Initializing agent...'}</span>
</div> </div>
</div> </div>
)} )}
@@ -967,8 +967,8 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<Tabs value={activeHooksTab} onValueChange={setActiveHooksTab} className="flex-1 flex flex-col overflow-hidden"> <Tabs value={activeHooksTab} onValueChange={setActiveHooksTab} className="flex-1 flex flex-col overflow-hidden">
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="project">Project Settings</TabsTrigger> <TabsTrigger value="project">{t('agents.projectSettings') || 'Project Settings'}</TabsTrigger>
<TabsTrigger value="local">Local Settings</TabsTrigger> <TabsTrigger value="local">{t('agents.localSettings') || 'Local Settings'}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="project" className="flex-1 overflow-auto"> <TabsContent value="project" className="flex-1 overflow-auto">

View File

@@ -237,7 +237,7 @@ export function AgentRunOutputViewer({
} }
} catch (error) { } catch (error) {
console.error('Failed to load agent output:', error); console.error('Failed to load agent output:', error);
setToast({ message: 'Failed to load agent output', type: 'error' }); setToast({ message: t('app.failedToLoadSessionOutput'), type: 'error' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -286,12 +286,12 @@ export function AgentRunOutputViewer({
}); });
const completeUnlisten = await listen<boolean>(`agent-complete:${run!.id}`, () => { const completeUnlisten = await listen<boolean>(`agent-complete:${run!.id}`, () => {
setToast({ message: 'Agent execution completed', type: 'success' }); setToast({ message: t('app.agentExecutionCompleted'), type: 'success' });
// Don't set status here as the parent component should handle it // Don't set status here as the parent component should handle it
}); });
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${run!.id}`, () => { const cancelUnlisten = await listen<boolean>(`agent-cancelled:${run!.id}`, () => {
setToast({ message: 'Agent execution was cancelled', type: 'error' }); setToast({ message: t('app.agentExecutionCancelled'), type: 'error' });
}); });
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten]; unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
@@ -305,7 +305,7 @@ export function AgentRunOutputViewer({
const jsonl = rawJsonlOutput.join('\n'); const jsonl = rawJsonlOutput.join('\n');
await navigator.clipboard.writeText(jsonl); await navigator.clipboard.writeText(jsonl);
setCopyPopoverOpen(false); setCopyPopoverOpen(false);
setToast({ message: 'Output copied as JSONL', type: 'success' }); setToast({ message: t('webview.sessionOutputCopiedJsonl'), type: 'success' });
}; };
const handleCopyAsMarkdown = async () => { const handleCopyAsMarkdown = async () => {
@@ -363,7 +363,7 @@ export function AgentRunOutputViewer({
await navigator.clipboard.writeText(markdown); await navigator.clipboard.writeText(markdown);
setCopyPopoverOpen(false); setCopyPopoverOpen(false);
setToast({ message: 'Output copied as Markdown', type: 'success' }); setToast({ message: t('webview.sessionOutputCopiedMarkdown'), type: 'success' });
}; };
const handleRefresh = async () => { const handleRefresh = async () => {
@@ -383,7 +383,7 @@ export function AgentRunOutputViewer({
const success = await api.killAgentSession(run.id); const success = await api.killAgentSession(run.id);
if (success) { if (success) {
setToast({ message: 'Agent execution stopped', type: 'success' }); setToast({ message: t('agentRun.executionStopped'), type: 'success' });
// Clean up listeners // Clean up listeners
unlistenRefs.current.forEach(unlisten => unlisten()); unlistenRefs.current.forEach(unlisten => unlisten());
@@ -410,14 +410,11 @@ export function AgentRunOutputViewer({
// Refresh the output to get updated status // Refresh the output to get updated status
await loadOutput(true); await loadOutput(true);
} else { } else {
setToast({ message: 'Failed to stop agent - it may have already finished', type: 'error' }); setToast({ message: t('agentRun.stopFailed'), type: 'error' });
} }
} catch (err) { } catch (err) {
console.error('[AgentRunOutputViewer] Failed to stop agent:', err); console.error('[AgentRunOutputViewer] Failed to stop agent:', err);
setToast({ setToast({ message: t('agentRun.stopFailed'), type: 'error' });
message: `Failed to stop execution: ${err instanceof Error ? err.message : 'Unknown error'}`,
type: 'error'
});
} }
}; };
@@ -515,7 +512,7 @@ export function AgentRunOutputViewer({
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading agent run...</p> <p className="text-muted-foreground">{t('app.loadingAgentRun')}</p>
</div> </div>
</div> </div>
); );
@@ -537,7 +534,7 @@ export function AgentRunOutputViewer({
{run.status === 'running' && ( {run.status === 'running' && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-xs text-green-600 font-medium">Running</span> <span className="text-xs text-green-600 font-medium">{t('agents.statusRunning')}</span>
</div> </div>
)} )}
</CardTitle> </CardTitle>
@@ -546,7 +543,7 @@ export function AgentRunOutputViewer({
</p> </p>
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-2"> <div className="flex items-center gap-3 text-xs text-muted-foreground mt-2">
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{run.model === 'opus' ? 'Claude 4.1 Opus' : 'Claude 4 Sonnet'} {run.model === 'opus' ? t('agents.opusName') : t('agents.sonnetName')}
</Badge> </Badge>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
@@ -579,7 +576,7 @@ export function AgentRunOutputViewer({
className="h-8 px-2" className="h-8 px-2"
> >
<Copy className="h-4 w-4 mr-1" /> <Copy className="h-4 w-4 mr-1" />
Copy {t('app.copyOutput')}
<ChevronDown className="h-3 w-3 ml-1" /> <ChevronDown className="h-3 w-3 ml-1" />
</Button> </Button>
} }
@@ -611,7 +608,7 @@ export function AgentRunOutputViewer({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setIsFullscreen(!isFullscreen)} onClick={() => setIsFullscreen(!isFullscreen)}
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"} title={isFullscreen ? t('webview.exitFullScreen') : t('webview.enterFullScreen')}
className="h-8 px-2" className="h-8 px-2"
> >
{isFullscreen ? ( {isFullscreen ? (
@@ -625,7 +622,7 @@ export function AgentRunOutputViewer({
size="sm" size="sm"
onClick={handleRefresh} onClick={handleRefresh}
disabled={refreshing} disabled={refreshing}
title="Refresh output" title={t('app.refresh')}
className="h-8 px-2" className="h-8 px-2"
> >
<RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} /> <RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
@@ -636,7 +633,7 @@ export function AgentRunOutputViewer({
size="sm" size="sm"
onClick={handleStop} onClick={handleStop}
disabled={refreshing} disabled={refreshing}
title="Stop execution" title={t('agents.stop')}
className="h-8 px-2 text-destructive hover:text-destructive" className="h-8 px-2 text-destructive hover:text-destructive"
> >
<StopCircle className="h-4 w-4" /> <StopCircle className="h-4 w-4" />
@@ -650,12 +647,12 @@ export function AgentRunOutputViewer({
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RefreshCw className="h-4 w-4 animate-spin" /> <RefreshCw className="h-4 w-4 animate-spin" />
<span>Loading output...</span> <span>{t('app.loadingOutput')}</span>
</div> </div>
</div> </div>
) : messages.length === 0 ? ( ) : messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground"> <div className="flex items-center justify-center h-full text-muted-foreground">
<p>No output available yet</p> <p>{t('app.noOutput')}</p>
</div> </div>
) : ( ) : (
<div <div
@@ -745,7 +742,7 @@ export function AgentRunOutputViewer({
disabled={refreshing} disabled={refreshing}
> >
<StopCircle className="h-4 w-4 mr-2" /> <StopCircle className="h-4 w-4 mr-2" />
Stop {t('agents.stop')}
</Button> </Button>
)} )}
<Button <Button
@@ -754,7 +751,7 @@ export function AgentRunOutputViewer({
onClick={() => setIsFullscreen(false)} onClick={() => setIsFullscreen(false)}
> >
<Minimize2 className="h-4 w-4 mr-2" /> <Minimize2 className="h-4 w-4 mr-2" />
Exit Fullscreen {t('webview.exitFullScreen')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -766,7 +763,7 @@ export function AgentRunOutputViewer({
<div className="max-w-4xl mx-auto space-y-2"> <div className="max-w-4xl mx-auto space-y-2">
{messages.length === 0 ? ( {messages.length === 0 ? (
<div className="text-center text-muted-foreground py-8"> <div className="text-center text-muted-foreground py-8">
No output available yet {t('app.noOutput')}
</div> </div>
) : ( ) : (
<> <>

View File

@@ -103,7 +103,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
} }
} catch (err) { } catch (err) {
console.error("Failed to load run:", err); console.error("Failed to load run:", err);
setError("Failed to load execution details"); setError(t('agentRun.loadFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -198,7 +198,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
type: "result", type: "result",
subtype: "error", subtype: "error",
is_error: true, is_error: true,
result: "Execution stopped by user", result: t('agentRun.executionStopped'),
duration_ms: 0, duration_ms: 0,
usage: { usage: {
input_tokens: 0, input_tokens: 0,
@@ -235,8 +235,8 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
if (error || !run) { if (error || !run) {
return ( return (
<div className={cn("flex flex-col items-center justify-center h-full", className)}> <div className={cn("flex flex-col items-center justify-center h-full", className)}>
<p className="text-destructive mb-4">{error || "Run not found"}</p> <p className="text-destructive mb-4">{error || t('agentRun.runNotFound')}</p>
<Button onClick={onBack}>Go Back</Button> <Button onClick={onBack}>{t('app.back')}</Button>
</div> </div>
); );
} }
@@ -264,7 +264,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
{renderIcon(run.agent_icon)} {renderIcon(run.agent_icon)}
<div> <div>
<h2 className="text-lg font-semibold">{run.agent_name}</h2> <h2 className="text-lg font-semibold">{run.agent_name}</h2>
<p className="text-xs text-muted-foreground">Execution History</p> <p className="text-xs text-muted-foreground">{t('agents.executionHistory')}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -278,7 +278,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
className="text-destructive hover:text-destructive" className="text-destructive hover:text-destructive"
> >
<StopCircle className="h-4 w-4 mr-1" /> <StopCircle className="h-4 w-4 mr-1" />
Stop {t('agents.stop')}
</Button> </Button>
)} )}
@@ -326,10 +326,10 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
<CardContent className="p-4"> <CardContent className="p-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="text-sm font-medium">Task:</h3> <h3 className="text-sm font-medium">{t('app.task')}:</h3>
<p className="text-sm text-muted-foreground flex-1">{run.task}</p> <p className="text-sm text-muted-foreground flex-1">{run.task}</p>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{run.model === 'opus' ? 'Claude 4.1 Opus' : 'Claude 4 Sonnet'} {run.model === 'opus' ? t('agents.opusName') : t('agents.sonnetName')}
</Badge> </Badge>
</div> </div>
@@ -349,7 +349,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
{run.metrics?.total_tokens && ( {run.metrics?.total_tokens && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Hash className="h-3 w-3" /> <Hash className="h-3 w-3" />
<span>{run.metrics.total_tokens} tokens</span> <span>{run.metrics.total_tokens} {t('usage.tokens')}</span>
</div> </div>
)} )}

View File

@@ -442,7 +442,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
variant="ghost" variant="ghost"
onClick={() => handleExportAgent(agent)} onClick={() => handleExportAgent(agent)}
className="flex items-center gap-1" className="flex items-center gap-1"
title="Export agent to .claudia.json" title={t('agents.exportToFile')}
> >
<Upload className="h-3 w-3" /> <Upload className="h-3 w-3" />
{t('agents.export')} {t('agents.export')}

View File

@@ -634,7 +634,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
const selected = await open({ const selected = await open({
directory: true, directory: true,
multiple: false, multiple: false,
title: "Select Project Directory" title: t('webview.selectProjectDirectory')
}); });
if (selected) { if (selected) {
@@ -644,7 +644,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
} catch (err) { } catch (err) {
console.error("Failed to select directory:", err); console.error("Failed to select directory:", err);
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);
setError(`Failed to select directory: ${errorMessage}`); setError(t('app.selectDirectoryFailed', { message: errorMessage }));
} }
}; };
@@ -652,7 +652,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath, claudeSessionId, effectiveSession }); console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath, claudeSessionId, effectiveSession });
if (!projectPath) { if (!projectPath) {
setError("Please select a project directory first"); setError(t('app.selectProjectFirst'));
return; return;
} }

View File

@@ -2,6 +2,7 @@ import React, { Component, ReactNode } from "react";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import i18n from "@/lib/i18n";
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
children: ReactNode; children: ReactNode;
@@ -51,14 +52,14 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<AlertCircle className="h-8 w-8 text-destructive flex-shrink-0 mt-0.5" /> <AlertCircle className="h-8 w-8 text-destructive flex-shrink-0 mt-0.5" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<h3 className="text-lg font-semibold">Something went wrong</h3> <h3 className="text-lg font-semibold">{i18n.t('errorBoundary.somethingWentWrong')}</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
An error occurred while rendering this component. {i18n.t('errorBoundary.errorOccurred')}
</p> </p>
{this.state.error.message && ( {this.state.error.message && (
<details className="mt-2"> <details className="mt-2">
<summary className="text-sm cursor-pointer text-muted-foreground hover:text-foreground"> <summary className="text-sm cursor-pointer text-muted-foreground hover:text-foreground">
Error details {i18n.t('errorBoundary.errorDetails')}
</summary> </summary>
<pre className="mt-2 text-xs bg-muted p-2 rounded overflow-auto"> <pre className="mt-2 text-xs bg-muted p-2 rounded overflow-auto">
{this.state.error.message} {this.state.error.message}
@@ -70,7 +71,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
size="sm" size="sm"
className="mt-4" className="mt-4"
> >
Try again {i18n.t('errorBoundary.tryAgain')}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { openUrl } from "@tauri-apps/plugin-opener"; import { openUrl } from "@tauri-apps/plugin-opener";
import asteriskLogo from "@/assets/nfo/asterisk-logo.png"; import asteriskLogo from "@/assets/nfo/asterisk-logo.png";
import keygennMusic from "@/assets/nfo/claudia-nfo.ogg"; import keygennMusic from "@/assets/nfo/claudia-nfo.ogg";
import { useTranslation } from "@/hooks/useTranslation";
interface NFOCreditsProps { interface NFOCreditsProps {
/** /**
@@ -22,6 +23,7 @@ interface NFOCreditsProps {
* <NFOCredits onClose={() => setShowNFO(false)} /> * <NFOCredits onClose={() => setShowNFO(false)} />
*/ */
export const NFOCredits: React.FC<NFOCreditsProps> = ({ onClose }) => { export const NFOCredits: React.FC<NFOCreditsProps> = ({ onClose }) => {
const { t } = useTranslation();
const audioRef = useRef<HTMLAudioElement | null>(null); const audioRef = useRef<HTMLAudioElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
@@ -159,10 +161,10 @@ export const NFOCredits: React.FC<NFOCreditsProps> = ({ onClose }) => {
await openUrl("https://github.com/getAsterisk/claudia/issues/new"); await openUrl("https://github.com/getAsterisk/claudia/issues/new");
}} }}
className="flex items-center gap-1 h-auto px-2 py-1" className="flex items-center gap-1 h-auto px-2 py-1"
title="File a bug" title={t('app.fileABug')}
> >
<Github className="h-3 w-3" /> <Github className="h-3 w-3" />
<span className="text-xs">File a bug</span> <span className="text-xs">{t('app.fileABug')}</span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -8,7 +8,8 @@ import {
Settings, Settings,
MoreVertical, MoreVertical,
Search, Search,
X X,
Plus
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
@@ -39,6 +40,10 @@ interface ProjectListProps {
* Callback when hooks configuration is clicked * Callback when hooks configuration is clicked
*/ */
onProjectSettings?: (project: Project) => void; onProjectSettings?: (project: Project) => void;
/**
* Callback when new session button is clicked
*/
onNewSession?: () => void;
/** /**
* Whether the list is currently loading * Whether the list is currently loading
*/ */
@@ -72,6 +77,7 @@ export const ProjectList: React.FC<ProjectListProps> = ({
projects, projects,
onProjectClick, onProjectClick,
onProjectSettings, onProjectSettings,
onNewSession,
className, className,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -107,16 +113,32 @@ export const ProjectList: React.FC<ProjectListProps> = ({
return ( return (
<div className={cn("space-y-4", className)}> <div className={cn("space-y-4", className)}>
{/* Search bar and results info */} {/* Action bar with new session button and search */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="flex flex-col lg:flex-row gap-3 items-stretch lg:items-center justify-between">
<div className="relative flex-1 max-w-md"> {/* New session button */}
{onNewSession && (
<Button
onClick={onNewSession}
size="default"
className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 lg:w-auto w-full group relative overflow-hidden"
>
{/* Gradient overlay effect */}
<div className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/10 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-500" />
<Plus className="mr-2 h-4 w-4 relative z-10" />
<span className="relative z-10">{t('newClaudeCodeSession')}</span>
</Button>
)}
{/* Search and results info */}
<div className="flex flex-col sm:flex-row sm:items-center gap-3 flex-1 lg:max-w-2xl">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t('searchProjects')} placeholder={t('searchProjects')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-9" className="pl-9 pr-9 h-10"
/> />
{searchQuery && ( {searchQuery && (
<Button <Button
@@ -130,8 +152,8 @@ export const ProjectList: React.FC<ProjectListProps> = ({
)} )}
</div> </div>
{/* Results info */} {/* Results info */}
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground whitespace-nowrap">
{searchQuery ? ( {searchQuery ? (
<span> <span>
{t('showingResults')}: <span className="font-semibold text-foreground">{filteredAndSortedProjects.length}</span> / {projects.length} {t('showingResults')}: <span className="font-semibold text-foreground">{filteredAndSortedProjects.length}</span> / {projects.length}
@@ -141,6 +163,7 @@ export const ProjectList: React.FC<ProjectListProps> = ({
{t('totalProjects')}: <span className="font-semibold text-foreground">{projects.length}</span> {t('totalProjects')}: <span className="font-semibold text-foreground">{projects.length}</span>
</span> </span>
)} )}
</div>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,7 @@
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { HooksEditor } from '@/components/HooksEditor'; import { HooksEditor } from '@/components/HooksEditor';
import { SlashCommandsManager } from '@/components/SlashCommandsManager'; import { SlashCommandsManager } from '@/components/SlashCommandsManager';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
@@ -33,6 +34,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
onBack, onBack,
className className
}) => { }) => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('commands'); const [activeTab, setActiveTab] = useState('commands');
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
@@ -89,7 +91,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
</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" />
<h2 className="text-xl font-semibold">Project Settings</h2> <h2 className="text-xl font-semibold">{t('agents.projectSettings')}</h2>
</div> </div>
</div> </div>
</div> </div>
@@ -109,15 +111,15 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
<TabsList className="mb-6"> <TabsList className="mb-6">
<TabsTrigger value="commands" className="gap-2"> <TabsTrigger value="commands" className="gap-2">
<Command className="h-4 w-4" /> <Command className="h-4 w-4" />
Slash Commands {t('slashCommands.slashCommands')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="project" className="gap-2"> <TabsTrigger value="project" className="gap-2">
<GitBranch className="h-4 w-4" /> <GitBranch className="h-4 w-4" />
Project Hooks {t('hooks.projectHooks', 'Project Hooks')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="local" className="gap-2"> <TabsTrigger value="local" className="gap-2">
<Shield className="h-4 w-4" /> <Shield className="h-4 w-4" />
Local Hooks {t('hooks.localHooks', 'Local Hooks')}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -125,7 +127,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="text-lg font-semibold mb-2">Project Slash Commands</h3> <h3 className="text-lg font-semibold mb-2">{t('slashCommands.projectSlashCommands')}</h3>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
Custom commands that are specific to this project. These commands are stored in Custom commands that are specific to this project. These commands are stored in
<code className="mx-1 px-2 py-1 bg-muted rounded text-xs">.claude/slash-commands/</code> <code className="mx-1 px-2 py-1 bg-muted rounded text-xs">.claude/slash-commands/</code>
@@ -145,7 +147,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="text-lg font-semibold mb-2">Project Hooks</h3> <h3 className="text-lg font-semibold mb-2">{t('hooks.projectHooks', 'Project Hooks')}</h3>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
These hooks apply to all users working on this project. They are stored in These hooks apply to all users working on this project. They are stored in
<code className="mx-1 px-2 py-1 bg-muted rounded text-xs">.claude/settings.json</code> <code className="mx-1 px-2 py-1 bg-muted rounded text-xs">.claude/settings.json</code>
@@ -165,7 +167,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="text-lg font-semibold mb-2">Local Hooks</h3> <h3 className="text-lg font-semibold mb-2">{t('hooks.localHooks', 'Local Hooks')}</h3>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
These hooks only apply to your machine. They are stored in These hooks only apply to your machine. They are stored in
<code className="mx-1 px-2 py-1 bg-muted rounded text-xs">.claude/settings.local.json</code> <code className="mx-1 px-2 py-1 bg-muted rounded text-xs">.claude/settings.local.json</code>
@@ -177,7 +179,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
<AlertTriangle className="h-5 w-5 text-yellow-600" /> <AlertTriangle className="h-5 w-5 text-yellow-600" />
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-yellow-600"> <p className="text-sm text-yellow-600">
Local settings file is not in .gitignore {t('projectSettings.gitignoreLocalWarning')}
</p> </p>
</div> </div>
<Button <Button
@@ -185,7 +187,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
variant="outline" variant="outline"
onClick={addToGitIgnore} onClick={addToGitIgnore}
> >
Add to .gitignore {t('projectSettings.addToGitignore')}
</Button> </Button>
</div> </div>
)} )}

View File

@@ -46,7 +46,7 @@ export const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({
setError(null); setError(null);
} catch (err) { } catch (err) {
console.error("Failed to load running sessions:", err); console.error("Failed to load running sessions:", err);
setError("Failed to load running sessions"); setError(t('runningSessions.loadFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -101,10 +101,10 @@ export const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" /> <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<h3 className="text-sm font-medium">Active Claude Sessions</h3> <h3 className="text-sm font-medium">{t('runningSessions.title')}</h3>
</div> </div>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
({runningSessions.length} running) {t('runningSessions.countRunning', { count: runningSessions.length })}
</span> </span>
</div> </div>
@@ -137,7 +137,7 @@ export const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({
{sessionId.substring(0, 20)}... {sessionId.substring(0, 20)}...
</p> </p>
<span className="text-xs text-green-600 font-medium"> <span className="text-xs text-green-600 font-medium">
Running {t('runningSessions.running')}
</span> </span>
</div> </div>
@@ -163,7 +163,7 @@ export const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({
className="flex-shrink-0" className="flex-shrink-0"
> >
<Play className="h-3 w-3 mr-1" /> <Play className="h-3 w-3 mr-1" />
Resume {t('runningSessions.resume')}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>

View File

@@ -8,6 +8,7 @@ import { ClaudeMemoriesDropdown } from "@/components/ClaudeMemoriesDropdown";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { formatUnixTimestamp, formatISOTimestamp, truncateText, getFirstLine } from "@/lib/date-utils"; import { formatUnixTimestamp, formatISOTimestamp, truncateText, getFirstLine } from "@/lib/date-utils";
import type { Session, ClaudeMdFile } from "@/lib/api"; import type { Session, ClaudeMdFile } from "@/lib/api";
import { useTranslation } from "@/hooks/useTranslation";
interface SessionListProps { interface SessionListProps {
/** /**
@@ -57,6 +58,7 @@ export const SessionList: React.FC<SessionListProps> = ({
onEditClaudeFile, onEditClaudeFile,
className, className,
}) => { }) => {
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
// Calculate pagination // Calculate pagination
@@ -89,7 +91,7 @@ export const SessionList: React.FC<SessionListProps> = ({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h2 className="text-base font-medium truncate">{projectPath}</h2> <h2 className="text-base font-medium truncate">{projectPath}</h2>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{sessions.length} session{sessions.length !== 1 ? 's' : ''} {t('projects.sessionCount', { count: sessions.length })}
</p> </p>
</div> </div>
</motion.div> </motion.div>
@@ -149,7 +151,7 @@ export const SessionList: React.FC<SessionListProps> = ({
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center space-x-1 text-xs text-muted-foreground"> <div className="flex items-center space-x-1 text-xs text-muted-foreground">
<MessageSquare className="h-3 w-3" /> <MessageSquare className="h-3 w-3" />
<span>First message:</span> <span>{t('sessions.firstMessage')}</span>
</div> </div>
<p className="text-xs line-clamp-2 text-foreground/80"> <p className="text-xs line-clamp-2 text-foreground/80">
{truncateText(getFirstLine(session.first_message), 100)} {truncateText(getFirstLine(session.first_message), 100)}
@@ -173,7 +175,7 @@ export const SessionList: React.FC<SessionListProps> = ({
{session.todo_data && ( {session.todo_data && (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
<span>Has todo</span> <span>{t('sessions.hasTodo')}</span>
</div> </div>
)} )}
</div> </div>

View File

@@ -189,7 +189,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
} }
} catch (error) { } catch (error) {
console.error('Failed to load session output:', error); console.error('Failed to load session output:', error);
setToast({ message: 'Failed to load session output', type: 'error' }); setToast({ message: t('app.failedToLoadSessionOutput'), type: 'error' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -223,12 +223,12 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
}); });
const completeUnlisten = await listen<boolean>(`agent-complete:${session.id}`, () => { const completeUnlisten = await listen<boolean>(`agent-complete:${session.id}`, () => {
setToast({ message: 'Agent execution completed', type: 'success' }); setToast({ message: t('app.agentExecutionCompleted'), type: 'success' });
// Don't set status here as the parent component should handle it // Don't set status here as the parent component should handle it
}); });
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${session.id}`, () => { const cancelUnlisten = await listen<boolean>(`agent-cancelled:${session.id}`, () => {
setToast({ message: 'Agent execution was cancelled', type: 'error' }); setToast({ message: t('app.agentExecutionCancelled'), type: 'error' });
}); });
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten]; unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
@@ -242,7 +242,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
const jsonl = rawJsonlOutput.join('\n'); const jsonl = rawJsonlOutput.join('\n');
await navigator.clipboard.writeText(jsonl); await navigator.clipboard.writeText(jsonl);
setCopyPopoverOpen(false); setCopyPopoverOpen(false);
setToast({ message: 'Output copied as JSONL', type: 'success' }); setToast({ message: t('webview.sessionOutputCopiedJsonl'), type: 'success' });
}; };
const handleCopyAsMarkdown = async () => { const handleCopyAsMarkdown = async () => {
@@ -297,7 +297,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
await navigator.clipboard.writeText(markdown); await navigator.clipboard.writeText(markdown);
setCopyPopoverOpen(false); setCopyPopoverOpen(false);
setToast({ message: 'Output copied as Markdown', type: 'success' }); setToast({ message: t('webview.sessionOutputCopiedMarkdown'), type: 'success' });
}; };
@@ -305,10 +305,10 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
setRefreshing(true); setRefreshing(true);
try { try {
await loadOutput(true); // Skip cache when manually refreshing await loadOutput(true); // Skip cache when manually refreshing
setToast({ message: 'Output refreshed', type: 'success' }); setToast({ message: t('app.outputRefreshed'), type: 'success' });
} catch (error) { } catch (error) {
console.error('Failed to refresh output:', error); console.error('Failed to refresh output:', error);
setToast({ message: 'Failed to refresh output', type: 'error' }); setToast({ message: t('app.failedToRefreshOutput'), type: 'error' });
} finally { } finally {
setRefreshing(false); setRefreshing(false);
} }
@@ -388,7 +388,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="text-2xl">{session.agent_icon}</div> <div className="text-2xl">{session.agent_icon}</div>
<div> <div>
<CardTitle className="text-base">{session.agent_name} - Output</CardTitle> <CardTitle className="text-base">{session.agent_name} - {t('app.output')}</CardTitle>
<div className="flex items-center space-x-2 mt-1"> <div className="flex items-center space-x-2 mt-1">
<Badge variant={session.status === 'running' ? 'default' : 'secondary'}> <Badge variant={session.status === 'running' ? 'default' : 'secondary'}>
{session.status} {session.status}
@@ -396,7 +396,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
{session.status === 'running' && ( {session.status === 'running' && (
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-200"> <Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-200">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse mr-1"></div> <div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse mr-1"></div>
Live {t('agentRun.live')}
</Badge> </Badge>
)} )}
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
@@ -459,7 +459,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
size="sm" size="sm"
onClick={refreshOutput} onClick={refreshOutput}
disabled={refreshing} disabled={refreshing}
title="Refresh output" title={t('app.refresh')}
> >
<RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} /> <RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
</Button> </Button>
@@ -474,7 +474,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RefreshCw className="h-4 w-4 animate-spin" /> <RefreshCw className="h-4 w-4 animate-spin" />
<span>Loading output...</span> <span>{t('app.loadingOutput')}</span>
</div> </div>
</div> </div>
) : ( ) : (
@@ -498,14 +498,14 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
{session.status === 'running' ? ( {session.status === 'running' ? (
<> <>
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground mb-2" /> <RefreshCw className="h-8 w-8 animate-spin text-muted-foreground mb-2" />
<p className="text-muted-foreground">Waiting for output...</p> <p className="text-muted-foreground">{t('app.waitingForOutput')}</p>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
Agent is running but no output received yet {t('app.agentRunningNoOutput')}
</p> </p>
</> </>
) : ( ) : (
<> <>
<p className="text-muted-foreground">No output available</p> <p className="text-muted-foreground">{t('app.noOutput')}</p>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -514,7 +514,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
disabled={refreshing} disabled={refreshing}
> >
{refreshing ? <RefreshCw className="h-4 w-4 animate-spin mr-2" /> : <RotateCcw className="h-4 w-4 mr-2" />} {refreshing ? <RefreshCw className="h-4 w-4 animate-spin mr-2" /> : <RotateCcw className="h-4 w-4 mr-2" />}
Refresh {t('app.refresh')}
</Button> </Button>
</> </>
)} )}
@@ -551,11 +551,11 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
<div className="flex items-center justify-between p-4 border-b border-border"> <div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="text-2xl">{session.agent_icon}</div> <div className="text-2xl">{session.agent_icon}</div>
<h2 className="text-lg font-semibold">{session.agent_name} - Output</h2> <h2 className="text-lg font-semibold">{session.agent_name} - {t('app.output')}</h2>
{session.status === 'running' && ( {session.status === 'running' && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-xs text-green-600 font-medium">Running</span> <span className="text-xs text-green-600 font-medium">{t('agents.statusRunning')}</span>
</div> </div>
)} )}
</div> </div>
@@ -605,7 +605,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
Close {t('app.close')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -632,14 +632,14 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
{session.status === 'running' ? ( {session.status === 'running' ? (
<> <>
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground mb-2" /> <RefreshCw className="h-8 w-8 animate-spin text-muted-foreground mb-2" />
<p className="text-muted-foreground">Waiting for output...</p> <p className="text-muted-foreground">{t('app.waitingForOutput')}</p>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
Agent is running but no output received yet {t('app.agentRunningNoOutput')}
</p> </p>
</> </>
) : ( ) : (
<> <>
<p className="text-muted-foreground">No output available</p> <p className="text-muted-foreground">{t('app.noOutput')}</p>
</> </>
)} )}
</div> </div>

View File

@@ -658,19 +658,37 @@ export const Settings: React.FC<SettingsProps> = ({
{/* Cleanup Period */} {/* Cleanup Period */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cleanup">{t('settings.generalOptions.chatRetention')}</Label> <Label htmlFor="cleanup">{t('settings.generalOptions.chatRetention')}</Label>
<Input <div className="flex items-center gap-3">
id="cleanup" <Input
type="number" id="cleanup"
min="1" type="number"
placeholder="30" min="1"
value={settings?.cleanupPeriodDays || ""} placeholder="30"
onChange={(e) => { value={settings?.cleanupPeriodDays === -1 ? "" : (settings?.cleanupPeriodDays || "")}
const value = e.target.value ? parseInt(e.target.value) : undefined; onChange={(e) => {
updateSetting("cleanupPeriodDays", value); const value = e.target.value ? parseInt(e.target.value) : undefined;
}} updateSetting("cleanupPeriodDays", value);
/> }}
disabled={settings?.cleanupPeriodDays === -1}
className="flex-1"
/>
<div className="flex items-center gap-2">
<Switch
id="permanent-retention"
checked={settings?.cleanupPeriodDays === -1}
onCheckedChange={(checked) => {
updateSetting("cleanupPeriodDays", checked ? -1 : undefined);
}}
/>
<Label htmlFor="permanent-retention" className="text-sm cursor-pointer">
{t('settings.generalOptions.permanentRetention')}
</Label>
</div>
</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('settings.generalOptions.chatRetentionDesc')} {settings?.cleanupPeriodDays === -1
? t('settings.generalOptions.chatRetentionPermanent')
: t('settings.generalOptions.chatRetentionDesc')}
</p> </p>
</div> </div>

View File

@@ -133,7 +133,7 @@ export const StorageTab: React.FC = () => {
} }
} catch (err) { } catch (err) {
console.error("Failed to load tables:", err); console.error("Failed to load tables:", err);
setError("Failed to load tables"); setError(t('storageTab.loadTablesFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -158,7 +158,7 @@ export const StorageTab: React.FC = () => {
setCurrentPage(page); setCurrentPage(page);
} catch (err) { } catch (err) {
console.error("Failed to load table data:", err); console.error("Failed to load table data:", err);
setError("Failed to load table data"); setError(t('storageTab.loadTableDataFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -205,7 +205,7 @@ export const StorageTab: React.FC = () => {
setEditingRow(null); setEditingRow(null);
} catch (err) { } catch (err) {
console.error("Failed to update row:", err); console.error("Failed to update row:", err);
setError("Failed to update row"); setError(t('storageTab.updateRowFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -225,7 +225,7 @@ export const StorageTab: React.FC = () => {
setDeletingRow(null); setDeletingRow(null);
} catch (err) { } catch (err) {
console.error("Failed to delete row:", err); console.error("Failed to delete row:", err);
setError("Failed to delete row"); setError(t('storageTab.deleteRowFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -244,7 +244,7 @@ export const StorageTab: React.FC = () => {
setNewRow(null); setNewRow(null);
} catch (err) { } catch (err) {
console.error("Failed to insert row:", err); console.error("Failed to insert row:", err);
setError("Failed to insert row"); setError(t('storageTab.insertRowFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -269,7 +269,7 @@ export const StorageTab: React.FC = () => {
} }
} catch (err) { } catch (err) {
console.error("Failed to execute SQL:", err); console.error("Failed to execute SQL:", err);
setSqlError(err instanceof Error ? err.message : "Failed to execute SQL"); setSqlError(err instanceof Error ? err.message : t('storageTab.executeSqlFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -287,14 +287,14 @@ export const StorageTab: React.FC = () => {
setTableData(null); setTableData(null);
setShowResetConfirm(false); setShowResetConfirm(false);
setToast({ setToast({
message: "Database Reset Complete: The database has been restored to its default state with empty tables (agents, agent_runs, app_settings).", message: t('storageTab.resetSuccess'),
type: "success", type: "success",
}); });
} catch (err) { } catch (err) {
console.error("Failed to reset database:", err); console.error("Failed to reset database:", err);
setError("Failed to reset database"); setError(t('storageTab.resetDatabaseFailed'));
setToast({ setToast({
message: "Reset Failed: Failed to reset the database. Please try again.", message: t('storageTab.resetFailed'),
type: "error", type: "error",
}); });
} finally { } finally {
@@ -337,7 +337,7 @@ export const StorageTab: React.FC = () => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Database className="h-4 w-4 text-primary" /> <Database className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">Database Storage</h3> <h3 className="text-sm font-semibold">{t('storageTab.title')}</h3>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@@ -347,7 +347,7 @@ export const StorageTab: React.FC = () => {
className="gap-2 h-8 text-xs" className="gap-2 h-8 text-xs"
> >
<Terminal className="h-3 w-3" /> <Terminal className="h-3 w-3" />
SQL Query {t('storageTab.sqlQuery')}
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
@@ -356,7 +356,7 @@ export const StorageTab: React.FC = () => {
className="gap-2 h-8 text-xs" className="gap-2 h-8 text-xs"
> >
<RefreshCw className="h-3 w-3" /> <RefreshCw className="h-3 w-3" />
Reset DB {t('storageTab.resetDbShort')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -365,7 +365,7 @@ export const StorageTab: React.FC = () => {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Select value={selectedTable} onValueChange={setSelectedTable}> <Select value={selectedTable} onValueChange={setSelectedTable}>
<SelectTrigger className="w-[200px] h-8 text-xs"> <SelectTrigger className="w-[200px] h-8 text-xs">
<SelectValue placeholder="Select a table"> <SelectValue placeholder={t('storageTab.selectTable')}>
{selectedTable && ( {selectedTable && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Table className="h-3 w-3" /> <Table className="h-3 w-3" />
@@ -380,7 +380,7 @@ export const StorageTab: React.FC = () => {
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<span>{table.name}</span> <span>{table.name}</span>
<span className="text-[10px] text-muted-foreground ml-2"> <span className="text-[10px] text-muted-foreground ml-2">
{table.row_count} rows {table.row_count} {t('storageTab.rows')}
</span> </span>
</div> </div>
</SelectItem> </SelectItem>
@@ -391,7 +391,7 @@ export const StorageTab: React.FC = () => {
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-3 w-3 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-3 w-3 text-muted-foreground" />
<Input <Input
placeholder="Search in table..." placeholder={t('storageTab.searchInTable')}
value={searchQuery} value={searchQuery}
onChange={(e) => handleSearch(e.target.value)} onChange={(e) => handleSearch(e.target.value)}
className="pl-8 h-8 text-xs" className="pl-8 h-8 text-xs"
@@ -406,7 +406,7 @@ export const StorageTab: React.FC = () => {
className="gap-2 h-8 text-xs" className="gap-2 h-8 text-xs"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
New Row {t('storageTab.newRow')}
</Button> </Button>
)} )}
</div> </div>
@@ -437,7 +437,7 @@ export const StorageTab: React.FC = () => {
</th> </th>
))} ))}
<th className="px-3 py-2 text-right text-xs font-medium text-muted-foreground"> <th className="px-3 py-2 text-right text-xs font-medium text-muted-foreground">
Actions {t('storageTab.actions')}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -520,9 +520,11 @@ export const StorageTab: React.FC = () => {
{tableData.total_pages > 1 && ( {tableData.total_pages > 1 && (
<div className="flex items-center justify-between p-3 border-t"> <div className="flex items-center justify-between p-3 border-t">
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
Showing {(currentPage - 1) * pageSize + 1} to{" "} {t('storageTab.pagination.showing', {
{Math.min(currentPage * pageSize, tableData.total_rows)} of{" "} from: (currentPage - 1) * pageSize + 1,
{tableData.total_rows} rows to: Math.min(currentPage * pageSize, tableData.total_rows),
total: tableData.total_rows
})}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@@ -533,10 +535,10 @@ export const StorageTab: React.FC = () => {
className="h-7 text-xs" className="h-7 text-xs"
> >
<ChevronLeft className="h-3 w-3" /> <ChevronLeft className="h-3 w-3" />
Previous {t('app.previous')}
</Button> </Button>
<div className="text-xs"> <div className="text-xs">
Page {currentPage} of {tableData.total_pages} {t('storageTab.pagination.pageOf', { page: currentPage, total: tableData.total_pages })}
</div> </div>
<Button <Button
variant="outline" variant="outline"
@@ -545,7 +547,7 @@ export const StorageTab: React.FC = () => {
disabled={currentPage === tableData.total_pages} disabled={currentPage === tableData.total_pages}
className="h-7 text-xs" className="h-7 text-xs"
> >
Next {t('app.next')}
<ChevronRight className="h-3 w-3" /> <ChevronRight className="h-3 w-3" />
</Button> </Button>
</div> </div>
@@ -575,9 +577,9 @@ export const StorageTab: React.FC = () => {
<Dialog open={!!editingRow} onOpenChange={() => setEditingRow(null)}> <Dialog open={!!editingRow} onOpenChange={() => setEditingRow(null)}>
<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>Edit Row</DialogTitle> <DialogTitle>{t('storageTab.editRow')}</DialogTitle>
<DialogDescription> <DialogDescription>
Update the values for this row in the {selectedTable} table. {t('storageTab.editRowDesc', { table: selectedTable })}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{editingRow && tableData && ( {editingRow && tableData && (
@@ -588,7 +590,7 @@ export const StorageTab: React.FC = () => {
{column.name} {column.name}
{column.pk && ( {column.pk && (
<span className="text-xs text-muted-foreground ml-2"> <span className="text-xs text-muted-foreground ml-2">
(Primary Key) ({t('storageTab.primaryKey')})
</span> </span>
)} )}
</Label> </Label>
@@ -622,9 +624,9 @@ export const StorageTab: React.FC = () => {
/> />
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Type: {column.type_name} {t('storageTab.type')}: {column.type_name}
{column.notnull && ", NOT NULL"} {column.notnull && `, ${t('storageTab.notNull')}`}
{column.dflt_value && `, Default: ${column.dflt_value}`} {column.dflt_value && `, ${t('storageTab.default')}: ${column.dflt_value}`}
</p> </p>
</div> </div>
))} ))}
@@ -632,7 +634,7 @@ export const StorageTab: React.FC = () => {
)} )}
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setEditingRow(null)}> <Button variant="outline" onClick={() => setEditingRow(null)}>
Cancel {t('app.cancel')}
</Button> </Button>
<Button <Button
onClick={() => handleUpdateRow(editingRow!)} onClick={() => handleUpdateRow(editingRow!)}
@@ -641,7 +643,7 @@ export const StorageTab: React.FC = () => {
{loading ? ( {loading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
"Update" t('app.update')
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -652,9 +654,9 @@ export const StorageTab: React.FC = () => {
<Dialog open={!!newRow} onOpenChange={() => setNewRow(null)}> <Dialog open={!!newRow} onOpenChange={() => setNewRow(null)}>
<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>New Row</DialogTitle> <DialogTitle>{t('storageTab.newRow')}</DialogTitle>
<DialogDescription> <DialogDescription>
Add a new row to the {selectedTable} table. {t('storageTab.newRowDesc', { table: selectedTable })}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{newRow && tableData && ( {newRow && tableData && (
@@ -665,7 +667,7 @@ export const StorageTab: React.FC = () => {
{column.name} {column.name}
{column.notnull && ( {column.notnull && (
<span className="text-xs text-destructive ml-2"> <span className="text-xs text-destructive ml-2">
(Required) ({t('validation.required')})
</span> </span>
)} )}
</Label> </Label>
@@ -697,8 +699,8 @@ export const StorageTab: React.FC = () => {
/> />
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Type: {column.type_name} {t('storageTab.type')}: {column.type_name}
{column.dflt_value && `, Default: ${column.dflt_value}`} {column.dflt_value && `, ${t('storageTab.default')}: ${column.dflt_value}`}
</p> </p>
</div> </div>
))} ))}
@@ -706,7 +708,7 @@ export const StorageTab: React.FC = () => {
)} )}
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setNewRow(null)}> <Button variant="outline" onClick={() => setNewRow(null)}>
Cancel {t('app.cancel')}
</Button> </Button>
<Button <Button
onClick={() => handleInsertRow(newRow!)} onClick={() => handleInsertRow(newRow!)}
@@ -715,7 +717,7 @@ export const StorageTab: React.FC = () => {
{loading ? ( {loading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
"Insert" t('storageTab.insert')
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -726,10 +728,9 @@ export const StorageTab: React.FC = () => {
<Dialog open={!!deletingRow} onOpenChange={() => setDeletingRow(null)}> <Dialog open={!!deletingRow} onOpenChange={() => setDeletingRow(null)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Delete Row</DialogTitle> <DialogTitle>{t('storageTab.deleteRow')}</DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to delete this row? This action cannot be {t('storageTab.deleteRowConfirm')}
undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{deletingRow && ( {deletingRow && (
@@ -752,7 +753,7 @@ export const StorageTab: React.FC = () => {
)} )}
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setDeletingRow(null)}> <Button variant="outline" onClick={() => setDeletingRow(null)}>
Cancel {t('app.cancel')}
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
@@ -762,7 +763,7 @@ export const StorageTab: React.FC = () => {
{loading ? ( {loading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
"Delete" t('app.delete')
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -773,18 +774,15 @@ export const StorageTab: React.FC = () => {
<Dialog open={showResetConfirm} onOpenChange={setShowResetConfirm}> <Dialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Reset Database</DialogTitle> <DialogTitle>{t('storageTab.resetDatabaseTitle')}</DialogTitle>
<DialogDescription> <DialogDescription>
This will delete all data and recreate the database with its default structure {t('storageTab.resetDatabaseDesc')}
(empty tables for agents, agent_runs, and app_settings). The database will be
restored to the same state as when you first installed the app. This action
cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex items-center gap-3 p-4 rounded-md bg-destructive/10 text-destructive"> <div className="flex items-center gap-3 p-4 rounded-md bg-destructive/10 text-destructive">
<AlertTriangle className="h-5 w-5" /> <AlertTriangle className="h-5 w-5" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
All your agents, runs, and settings will be permanently deleted! {t('storageTab.resetWarning')}
</span> </span>
</div> </div>
<DialogFooter> <DialogFooter>
@@ -792,7 +790,7 @@ export const StorageTab: React.FC = () => {
variant="outline" variant="outline"
onClick={() => setShowResetConfirm(false)} onClick={() => setShowResetConfirm(false)}
> >
Cancel {t('app.cancel')}
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
@@ -802,7 +800,7 @@ export const StorageTab: React.FC = () => {
{loading ? ( {loading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
"Reset Database" t('storageTab.resetDatabaseTitle')
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -813,19 +811,19 @@ export const StorageTab: React.FC = () => {
<Dialog open={showSqlEditor} onOpenChange={setShowSqlEditor}> <Dialog open={showSqlEditor} onOpenChange={setShowSqlEditor}>
<DialogContent className="max-w-4xl max-h-[80vh]"> <DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader> <DialogHeader>
<DialogTitle>SQL Query Editor</DialogTitle> <DialogTitle>{t('storageTab.sqlEditorTitle')}</DialogTitle>
<DialogDescription> <DialogDescription>
Execute raw SQL queries on the database. Use with caution. {t('storageTab.sqlEditorDesc')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="sql-query">SQL Query</Label> <Label htmlFor="sql-query">{t('storageTab.sqlQuery')}</Label>
<Textarea <Textarea
id="sql-query" id="sql-query"
value={sqlQuery} value={sqlQuery}
onChange={(e) => setSqlQuery(e.target.value)} onChange={(e) => setSqlQuery(e.target.value)}
placeholder="SELECT * FROM agents LIMIT 10;" placeholder={t('storageTab.sqlQueryPlaceholder')}
className="font-mono text-sm h-32" className="font-mono text-sm h-32"
/> />
</div> </div>
@@ -845,11 +843,10 @@ export const StorageTab: React.FC = () => {
<div className="p-3 rounded-md bg-green-500/10 text-green-600 dark:text-green-400 text-sm"> <div className="p-3 rounded-md bg-green-500/10 text-green-600 dark:text-green-400 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
Query executed successfully. {sqlResult.rows_affected} rows {t('storageTab.queryExecuted')} {sqlResult.rows_affected} {t('storageTab.rowsAffected')}
affected.
{sqlResult.last_insert_rowid && ( {sqlResult.last_insert_rowid && (
<span> <span>
Last insert ID: {sqlResult.last_insert_rowid} {t('storageTab.lastInsertId')}: {sqlResult.last_insert_rowid}
</span> </span>
)} )}
</div> </div>
@@ -927,7 +924,7 @@ export const StorageTab: React.FC = () => {
setSqlError(null); setSqlError(null);
}} }}
> >
Close {t('app.close')}
</Button> </Button>
<Button <Button
onClick={handleExecuteSql} onClick={handleExecuteSql}

View File

@@ -5,6 +5,7 @@ import { useTabState } from '@/hooks/useTabState';
import { Tab, useTabContext } from '@/contexts/TabContext'; import { Tab, useTabContext } from '@/contexts/TabContext';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTrackEvent } from '@/hooks'; import { useTrackEvent } from '@/hooks';
import { useTranslation as useAppTranslation } from '@/hooks/useTranslation';
interface TabItemProps { interface TabItemProps {
tab: Tab; tab: Tab;
@@ -16,6 +17,7 @@ interface TabItemProps {
} }
const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDragging = false, setDraggedTabId }) => { const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDragging = false, setDraggedTabId }) => {
const { t } = useAppTranslation();
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const getIcon = () => { const getIcon = () => {
@@ -103,7 +105,7 @@ const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDr
{tab.hasUnsavedChanges && !statusIcon && ( {tab.hasUnsavedChanges && !statusIcon && (
<span <span
className="w-1.5 h-1.5 bg-primary rounded-full" className="w-1.5 h-1.5 bg-primary rounded-full"
title="Unsaved changes" title={t('app.unsavedChanges')}
/> />
)} )}
</div> </div>
@@ -120,7 +122,7 @@ const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDr
"focus:outline-none focus:ring-1 focus:ring-destructive/50", "focus:outline-none focus:ring-1 focus:ring-destructive/50",
(isHovered || isActive) ? "opacity-100" : "opacity-0" (isHovered || isActive) ? "opacity-100" : "opacity-0"
)} )}
title={`Close ${tab.title}`} title={`${t('app.close')} ${tab.title}`}
tabIndex={-1} tabIndex={-1}
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
@@ -135,6 +137,7 @@ interface TabManagerProps {
} }
export const TabManager: React.FC<TabManagerProps> = ({ className }) => { export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
const { t } = useAppTranslation();
const { const {
tabs, tabs,
activeTabId, activeTabId,
@@ -342,7 +345,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
"transition-colors duration-200 flex items-center justify-center", "transition-colors duration-200 flex items-center justify-center",
"bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60" "bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60"
)} )}
title="Scroll tabs left" title={t('tabs.scrollLeft')}
> >
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M15 18l-6-6 6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" /> <path d="M15 18l-6-6 6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
@@ -396,7 +399,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
"transition-colors duration-200 flex items-center justify-center", "transition-colors duration-200 flex items-center justify-center",
"bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60" "bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60"
)} )}
title="Scroll tabs right" title={t('tabs.scrollRight')}
> >
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M9 18l6-6-6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" /> <path d="M9 18l6-6-6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
@@ -416,7 +419,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
? "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"
)} )}
title={canAddTab() ? "Browse projects (Ctrl+T)" : `Maximum tabs reached (${tabs.length}/20)`} title={canAddTab() ? t('tabs.browseProjectsShortcut') : t('tabs.maximumTabsReached', { count: tabs.length })}
> >
<Plus className="w-3.5 h-3.5" /> <Plus className="w-3.5 h-3.5" />
</button> </button>

View File

@@ -294,7 +294,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
{isCurrent && ( {isCurrent && (
<Badge variant="default" className="text-xs">Current</Badge> <Badge variant="default" className="text-xs">{t('checkpoint.current') || 'Current'}</Badge>
)} )}
<span className="text-xs font-mono text-muted-foreground"> <span className="text-xs font-mono text-muted-foreground">
{node.checkpoint.id.slice(0, 8)} {node.checkpoint.id.slice(0, 8)}
@@ -309,7 +309,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
)} )}
<p className="text-xs text-muted-foreground line-clamp-2"> <p className="text-xs text-muted-foreground line-clamp-2">
{node.checkpoint.metadata.userPrompt || "No prompt"} {node.checkpoint.metadata.userPrompt || t('checkpoint.noPrompt') || 'No prompt'}
</p> </p>
<div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground"> <div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
@@ -341,7 +341,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
<RotateCcw className="h-3 w-3" /> <RotateCcw className="h-3 w-3" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Restore to this checkpoint</TooltipContent> <TooltipContent>{t('checkpoint.restoreToThis') || 'Restore to this checkpoint'}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -360,7 +360,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
<GitFork className="h-3 w-3" /> <GitFork className="h-3 w-3" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Fork from this checkpoint</TooltipContent> <TooltipContent>{t('checkpoint.forkFromThis') || 'Fork from this checkpoint'}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -379,7 +379,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
<Diff className="h-3 w-3" /> <Diff className="h-3 w-3" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Compare with another checkpoint</TooltipContent> <TooltipContent>{t('checkpoint.compareWithAnother') || 'Compare with another checkpoint'}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
@@ -442,7 +442,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
disabled={isLoading} disabled={isLoading}
> >
<Save className="h-3 w-3 mr-1" /> <Save className="h-3 w-3 mr-1" />
Checkpoint {t('checkpoint.createCheckpoint')}
</Button> </Button>
</div> </div>
@@ -469,18 +469,18 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}> <Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Checkpoint</DialogTitle> <DialogTitle>{t('checkpoint.createCheckpoint')}</DialogTitle>
<DialogDescription> <DialogDescription>
Save the current state of your session with an optional description. {t('checkpoint.createCheckpointDesc') || 'Save the current state of your session with an optional description.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="description">Description (optional)</Label> <Label htmlFor="description">{t('checkpoint.descriptionOptional') || 'Description (optional)'}</Label>
<Input <Input
id="description" id="description"
placeholder="e.g., Before major refactoring" placeholder={t('checkpoint.descriptionPlaceholder') || 'e.g., Before major refactoring'}
value={checkpointDescription} value={checkpointDescription}
onChange={(e) => setCheckpointDescription(e.target.value)} onChange={(e) => setCheckpointDescription(e.target.value)}
onKeyPress={(e) => { onKeyPress={(e) => {
@@ -498,13 +498,13 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
onClick={() => setShowCreateDialog(false)} onClick={() => setShowCreateDialog(false)}
disabled={isLoading} disabled={isLoading}
> >
Cancel {t('app.cancel')}
</Button> </Button>
<Button <Button
onClick={handleCreateCheckpoint} onClick={handleCreateCheckpoint}
disabled={isLoading} disabled={isLoading}
> >
Create Checkpoint {t('checkpoint.createCheckpoint')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -514,7 +514,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
<Dialog open={showDiffDialog} onOpenChange={setShowDiffDialog}> <Dialog open={showDiffDialog} onOpenChange={setShowDiffDialog}>
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Checkpoint Comparison</DialogTitle> <DialogTitle>{t('checkpoint.checkpointComparison') || 'Checkpoint Comparison'}</DialogTitle>
<DialogDescription> <DialogDescription>
Changes between "{selectedCheckpoint?.description || selectedCheckpoint?.id.slice(0, 8)}" Changes between "{selectedCheckpoint?.description || selectedCheckpoint?.id.slice(0, 8)}"
and "{compareCheckpoint?.description || compareCheckpoint?.id.slice(0, 8)}" and "{compareCheckpoint?.description || compareCheckpoint?.id.slice(0, 8)}"
@@ -527,19 +527,19 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<Card> <Card>
<CardContent className="p-3"> <CardContent className="p-3">
<div className="text-xs text-muted-foreground">Modified Files</div> <div className="text-xs text-muted-foreground">{t('checkpoint.modifiedFiles') || 'Modified Files'}</div>
<div className="text-2xl font-bold">{diff.modifiedFiles.length}</div> <div className="text-2xl font-bold">{diff.modifiedFiles.length}</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-3"> <CardContent className="p-3">
<div className="text-xs text-muted-foreground">Added Files</div> <div className="text-xs text-muted-foreground">{t('checkpoint.addedFiles') || 'Added Files'}</div>
<div className="text-2xl font-bold text-green-600">{diff.addedFiles.length}</div> <div className="text-2xl font-bold text-green-600">{diff.addedFiles.length}</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-3"> <CardContent className="p-3">
<div className="text-xs text-muted-foreground">Deleted Files</div> <div className="text-xs text-muted-foreground">{t('checkpoint.deletedFiles') || 'Deleted Files'}</div>
<div className="text-2xl font-bold text-red-600">{diff.deletedFiles.length}</div> <div className="text-2xl font-bold text-red-600">{diff.deletedFiles.length}</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -548,14 +548,14 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
{/* Token delta */} {/* Token delta */}
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Badge variant={diff.tokenDelta > 0 ? "default" : "secondary"}> <Badge variant={diff.tokenDelta > 0 ? "default" : "secondary"}>
{diff.tokenDelta > 0 ? "+" : ""}{diff.tokenDelta.toLocaleString()} tokens {diff.tokenDelta > 0 ? "+" : ""}{diff.tokenDelta.toLocaleString()} {t('usage.tokens')}
</Badge> </Badge>
</div> </div>
{/* File lists */} {/* File lists */}
{diff.modifiedFiles.length > 0 && ( {diff.modifiedFiles.length > 0 && (
<div> <div>
<h4 className="text-sm font-medium mb-2">Modified Files</h4> <h4 className="text-sm font-medium mb-2">{t('checkpoint.modifiedFiles') || 'Modified Files'}</h4>
<div className="space-y-1"> <div className="space-y-1">
{diff.modifiedFiles.map((file) => ( {diff.modifiedFiles.map((file) => (
<div key={file.path} className="flex items-center justify-between text-xs"> <div key={file.path} className="flex items-center justify-between text-xs">
@@ -572,7 +572,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
{diff.addedFiles.length > 0 && ( {diff.addedFiles.length > 0 && (
<div> <div>
<h4 className="text-sm font-medium mb-2">Added Files</h4> <h4 className="text-sm font-medium mb-2">{t('checkpoint.addedFiles') || 'Added Files'}</h4>
<div className="space-y-1"> <div className="space-y-1">
{diff.addedFiles.map((file) => ( {diff.addedFiles.map((file) => (
<div key={file} className="text-xs font-mono text-green-600"> <div key={file} className="text-xs font-mono text-green-600">
@@ -585,7 +585,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
{diff.deletedFiles.length > 0 && ( {diff.deletedFiles.length > 0 && (
<div> <div>
<h4 className="text-sm font-medium mb-2">Deleted Files</h4> <h4 className="text-sm font-medium mb-2">{t('checkpoint.deletedFiles') || 'Deleted Files'}</h4>
<div className="space-y-1"> <div className="space-y-1">
{diff.deletedFiles.map((file) => ( {diff.deletedFiles.map((file) => (
<div key={file} className="text-xs font-mono text-red-600"> <div key={file} className="text-xs font-mono text-red-600">
@@ -607,7 +607,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
setCompareCheckpoint(null); setCompareCheckpoint(null);
}} }}
> >
Close {t('app.close')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@@ -635,6 +635,7 @@ export const BashWidget: React.FC<{
description?: string; description?: string;
result?: any; result?: any;
}> = ({ command, description, result }) => { }> = ({ command, description, result }) => {
const { t } = useTranslation();
// Extract result content if available // Extract result content if available
let resultContent = ''; let resultContent = '';
let isError = false; let isError = false;
@@ -660,7 +661,7 @@ export const BashWidget: React.FC<{
<div className="rounded-lg border bg-zinc-950 overflow-hidden"> <div className="rounded-lg border bg-zinc-950 overflow-hidden">
<div className="px-4 py-2 bg-zinc-900/50 flex items-center gap-2 border-b"> <div className="px-4 py-2 bg-zinc-900/50 flex items-center gap-2 border-b">
<Terminal className="h-3.5 w-3.5 text-green-500" /> <Terminal className="h-3.5 w-3.5 text-green-500" />
<span className="text-xs font-mono text-muted-foreground">Terminal</span> <span className="text-xs font-mono text-muted-foreground">{t('widgets.terminal.title')}</span>
{description && ( {description && (
<> <>
<ChevronRight className="h-3 w-3 text-muted-foreground" /> <ChevronRight className="h-3 w-3 text-muted-foreground" />
@@ -671,7 +672,7 @@ export const BashWidget: React.FC<{
{!result && ( {!result && (
<div className="ml-auto flex items-center gap-1 text-xs text-muted-foreground"> <div className="ml-auto flex items-center gap-1 text-xs text-muted-foreground">
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse" /> <div className="h-2 w-2 bg-green-500 rounded-full animate-pulse" />
<span>Running...</span> <span>{t('widgets.common.running')}</span>
</div> </div>
)} )}
</div> </div>
@@ -688,7 +689,7 @@ export const BashWidget: React.FC<{
? "border-red-500/20 bg-red-500/5 text-red-400" ? "border-red-500/20 bg-red-500/5 text-red-400"
: "border-green-500/20 bg-green-500/5 text-green-300" : "border-green-500/20 bg-green-500/5 text-green-300"
)}> )}>
{resultContent || (isError ? "Command failed" : "Command completed")} {resultContent || (isError ? t('widgets.bash.commandFailed') : t('widgets.bash.commandCompleted'))}
</div> </div>
)} )}
</div> </div>

View File

@@ -112,7 +112,7 @@ export const Topbar: React.FC<TopbarProps> = ({
// Emit event to return to home // Emit event to return to home
window.dispatchEvent(new CustomEvent('switch-to-welcome')); window.dispatchEvent(new CustomEvent('switch-to-welcome'));
}} }}
title="Return to Home" title={t('app.returnHome')}
> >
<div className="flex items-center space-x-2 text-xs"> <div className="flex items-center space-x-2 text-xs">
<Circle <Circle

View File

@@ -172,7 +172,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
className={cn("flex flex-col h-full bg-background border-l", className)} className={cn("flex flex-col h-full bg-background border-l", className)}
tabIndex={-1} tabIndex={-1}
role="region" role="region"
aria-label="Web preview" aria-label={t('webview.preview')}
> >
{/* Browser Top Bar */} {/* Browser Top Bar */}
<div className="border-b bg-muted/30 flex-shrink-0"> <div className="border-b bg-muted/30 flex-shrink-0">
@@ -322,7 +322,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
ref={iframeRef} ref={iframeRef}
src={currentUrl} src={currentUrl}
className="absolute inset-0 w-full h-full border-0" className="absolute inset-0 w-full h-full border-0"
title="Preview" title={t('webview.preview')}
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox" sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
onLoad={() => setIsLoading(false)} onLoad={() => setIsLoading(false)}
onError={() => { onError={() => {

View File

@@ -90,7 +90,7 @@ export const SessionHeader: React.FC<SessionHeaderProps> = React.memo(({
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<FolderOpen className="h-4 w-4" /> <FolderOpen className="h-4 w-4" />
Select Project {t('webview.selectProjectDirectory')}
</Button> </Button>
)} )}
</div> </div>
@@ -104,7 +104,7 @@ export const SessionHeader: React.FC<SessionHeaderProps> = React.memo(({
</Badge> </Badge>
{totalTokens > 0 && ( {totalTokens > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{totalTokens.toLocaleString()} tokens {totalTokens.toLocaleString()} {t('usage.tokens')}
</Badge> </Badge>
)} )}
</div> </div>
@@ -165,13 +165,13 @@ export const SessionHeader: React.FC<SessionHeaderProps> = React.memo(({
{onProjectSettings && projectPath && ( {onProjectSettings && projectPath && (
<DropdownMenuItem onClick={onProjectSettings}> <DropdownMenuItem onClick={onProjectSettings}>
<Settings className="h-4 w-4 mr-2" /> <Settings className="h-4 w-4 mr-2" />
Project Settings {t('agents.projectSettings')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{onSlashCommandsSettings && projectPath && ( {onSlashCommandsSettings && projectPath && (
<DropdownMenuItem onClick={onSlashCommandsSettings}> <DropdownMenuItem onClick={onSlashCommandsSettings}>
<Command className="h-4 w-4 mr-2" /> <Command className="h-4 w-4 mr-2" />
Slash Commands {t('slashCommands.slashCommands')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { Terminal, ChevronRight } from "lucide-react"; import { Terminal, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslation } from "@/hooks/useTranslation";
interface BashWidgetProps { interface BashWidgetProps {
command: string; command: string;
@@ -9,6 +10,7 @@ interface BashWidgetProps {
} }
export const BashWidget: React.FC<BashWidgetProps> = ({ command, description, result }) => { export const BashWidget: React.FC<BashWidgetProps> = ({ command, description, result }) => {
const { t } = useTranslation();
// Extract result content if available // Extract result content if available
let resultContent = ''; let resultContent = '';
let isError = false; let isError = false;
@@ -34,7 +36,7 @@ export const BashWidget: React.FC<BashWidgetProps> = ({ command, description, re
<div className="rounded-lg border bg-zinc-950 overflow-hidden"> <div className="rounded-lg border bg-zinc-950 overflow-hidden">
<div className="px-4 py-2 bg-zinc-900/50 flex items-center gap-2 border-b"> <div className="px-4 py-2 bg-zinc-900/50 flex items-center gap-2 border-b">
<Terminal className="h-3.5 w-3.5 text-green-500" /> <Terminal className="h-3.5 w-3.5 text-green-500" />
<span className="text-xs font-mono text-muted-foreground">Terminal</span> <span className="text-xs font-mono text-muted-foreground">{t('widgets.terminal.title')}</span>
{description && ( {description && (
<> <>
<ChevronRight className="h-3 w-3 text-muted-foreground" /> <ChevronRight className="h-3 w-3 text-muted-foreground" />
@@ -45,7 +47,7 @@ export const BashWidget: React.FC<BashWidgetProps> = ({ command, description, re
{!result && ( {!result && (
<div className="ml-auto flex items-center gap-1 text-xs text-muted-foreground"> <div className="ml-auto flex items-center gap-1 text-xs text-muted-foreground">
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse" /> <div className="h-2 w-2 bg-green-500 rounded-full animate-pulse" />
<span>Running...</span> <span>{t('widgets.common.running')}</span>
</div> </div>
)} )}
</div> </div>
@@ -62,7 +64,7 @@ export const BashWidget: React.FC<BashWidgetProps> = ({ command, description, re
? "border-red-500/20 bg-red-500/5 text-red-400" ? "border-red-500/20 bg-red-500/5 text-red-400"
: "border-green-500/20 bg-green-500/5 text-green-300" : "border-green-500/20 bg-green-500/5 text-green-300"
)}> )}>
{resultContent || (isError ? "Command failed" : "Command completed")} {resultContent || (isError ? t('widgets.bash.commandFailed') : t('widgets.bash.commandCompleted'))}
</div> </div>
)} )}
</div> </div>

View File

@@ -12,6 +12,7 @@
"name": "Claudia", "name": "Claudia",
"welcome": "Welcome to Claudia", "welcome": "Welcome to Claudia",
"tagline": "Powerful Claude Code session management tool", "tagline": "Powerful Claude Code session management tool",
"returnHome": "Return to Home",
"loading": "Loading...", "loading": "Loading...",
"error": "Error", "error": "Error",
"success": "Success", "success": "Success",
@@ -99,8 +100,22 @@
"justNow": "just now", "justNow": "just now",
"minutesAgo": "{{count}} minute{{plural}} ago", "minutesAgo": "{{count}} minute{{plural}} ago",
"hoursAgo": "{{count}} hour{{plural}} ago", "hoursAgo": "{{count}} hour{{plural}} ago",
"daysAgo": "{{count}} day{{plural}} ago" "daysAgo": "{{count}} day{{plural}} ago",
"loadingOutput": "Loading output...",
"waitingForOutput": "Waiting for output...",
"noOutput": "No output available",
"agentRunningNoOutput": "Agent is running but no output received yet",
"failedToLoadSessionOutput": "Failed to load session output",
"outputRefreshed": "Output refreshed",
"fileABug": "File a bug",
"failedToRefreshOutput": "Failed to refresh output",
"loadingAgentRun": "Loading agent run...",
"agentExecutionCompleted": "Agent execution completed",
"agentExecutionCancelled": "Agent execution was cancelled",
"selectProjectFirst": "Please select a project directory first",
"selectDirectoryFailed": "Failed to select directory: {{message}}"
}, },
"claudeSession": {},
"navigation": { "navigation": {
"projects": "CC Projects", "projects": "CC Projects",
"agents": "Agent Management", "agents": "Agent Management",
@@ -138,7 +153,27 @@
"sessions": "Sessions", "sessions": "Sessions",
"noSessions": "No sessions found", "noSessions": "No sessions found",
"lastModified": "Last Modified", "lastModified": "Last Modified",
"sessionHistory": "Session History" "sessionHistory": "Session History",
"sessionCount": "{{count}} sessions"
},
"sessions": {
"firstMessage": "First message:",
"hasTodo": "Has todo"
},
"runningSessions": {
"title": "Active Claude Sessions",
"countRunning": "({{count}} running)",
"running": "Running",
"resume": "Resume",
"loadFailed": "Failed to load running sessions"
},
"widgets": {
"terminal": { "title": "Terminal" },
"common": { "running": "Running..." },
"bash": {
"commandFailed": "Command failed",
"commandCompleted": "Command completed"
}
}, },
"agents": { "agents": {
"title": "Agent Management", "title": "Agent Management",
@@ -153,6 +188,11 @@
"enterTask": "Enter the task for the agent", "enterTask": "Enter the task for the agent",
"hooks": "Hooks", "hooks": "Hooks",
"configureHooks": "Configure Hooks", "configureHooks": "Configure Hooks",
"configureHooksDesc": "Configure hooks that run before, during, and after tool executions. Changes are saved immediately.",
"projectSettings": "Project Settings",
"localSettings": "Local Settings",
"initializing": "Initializing agent...",
"exportToFile": "Export agent to .claudia.json",
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"stop": "Stop", "stop": "Stop",
"agentName": "Agent Name", "agentName": "Agent Name",
@@ -317,7 +357,13 @@
"condition": "Condition", "condition": "Condition",
"matchesRegex": "Matches regex", "matchesRegex": "Matches regex",
"message": "Message", "message": "Message",
"enterShellCommand": "Enter shell command..." "enterShellCommand": "Enter shell command...",
"projectHooks": "Project Hooks",
"localHooks": "Local Hooks"
},
"projectSettings": {
"gitignoreLocalWarning": "Local settings file is not in .gitignore",
"addToGitignore": "Add to .gitignore"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Settings",
@@ -380,6 +426,8 @@
"verboseOutputDesc": "Show full bash and command outputs", "verboseOutputDesc": "Show full bash and command outputs",
"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)",
"chatRetentionPermanent": "Permanent retention enabled, chat records will not be automatically deleted",
"permanentRetention": "Permanent Retention",
"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...",
@@ -761,6 +809,18 @@
"checkpointSettingsTitle": "Checkpoint Settings", "checkpointSettingsTitle": "Checkpoint Settings",
"experimentalFeature": "Experimental Feature", "experimentalFeature": "Experimental Feature",
"checkpointWarning": "Checkpointing may affect directory structure or cause data loss. Use with caution.", "checkpointWarning": "Checkpointing may affect directory structure or cause data loss. Use with caution.",
"createCheckpointDesc": "Save the current state of your session with an optional description.",
"descriptionOptional": "Description (optional)",
"descriptionPlaceholder": "e.g., Before major refactoring",
"current": "Current",
"restoreToThis": "Restore to this checkpoint",
"forkFromThis": "Fork from this checkpoint",
"compareWithAnother": "Compare with another checkpoint",
"checkpointComparison": "Checkpoint Comparison",
"modifiedFiles": "Modified Files",
"addedFiles": "Added Files",
"deletedFiles": "Deleted Files",
"noPrompt": "No prompt",
"automaticCheckpoints": "Automatic Checkpoints", "automaticCheckpoints": "Automatic Checkpoints",
"automaticCheckpointsDesc": "Automatically create checkpoints based on the selected strategy", "automaticCheckpointsDesc": "Automatically create checkpoints based on the selected strategy",
"checkpointStrategy": "Checkpoint Strategy", "checkpointStrategy": "Checkpoint Strategy",
@@ -967,4 +1027,60 @@
"warning": { "warning": {
"title": "Warning" "title": "Warning"
} }
,
"agentRun": {
"runNotFound": "Run not found",
"loadFailed": "Failed to load execution details",
"executionStopped": "Execution stopped by user",
"live": "Live",
"stopFailed": "Failed to stop agent - it may have already finished"
},
"tabs": {
"scrollLeft": "Scroll tabs left",
"scrollRight": "Scroll tabs right",
"browseProjectsShortcut": "Browse projects (Ctrl+T)",
"maximumTabsReached": "Maximum tabs reached ({{count}}/20)"
},
"storageTab": {
"title": "Database Storage",
"sqlQuery": "SQL Query",
"resetDbShort": "Reset DB",
"selectTable": "Select a table",
"rows": "rows",
"searchInTable": "Search in table...",
"newRow": "New Row",
"actions": "Actions",
"pagination": {
"showing": "Showing {{from}} to {{to}} of {{total}} rows",
"pageOf": "Page {{page}} of {{total}}"
},
"editRow": "Edit Row",
"editRowDesc": "Update the values for this row in the {{table}} table.",
"primaryKey": "Primary Key",
"type": "Type",
"notNull": "NOT NULL",
"default": "Default",
"insert": "Insert",
"newRowDesc": "Add a new row to the {{table}} table.",
"deleteRow": "Delete Row",
"deleteRowConfirm": "Are you sure you want to delete this row? This action cannot be undone.",
"resetDatabaseTitle": "Reset Database",
"resetDatabaseDesc": "This will delete all data and recreate the database with its default structure (empty tables for agents, agent_runs, and app_settings). The database will be restored to the same state as when you first installed the app. This action cannot be undone.",
"resetWarning": "All your agents, runs, and settings will be permanently deleted!",
"sqlEditorTitle": "SQL Query Editor",
"sqlEditorDesc": "Execute raw SQL queries on the database. Use with caution.",
"sqlQueryPlaceholder": "SELECT * FROM agents LIMIT 10;",
"queryExecuted": "Query executed successfully.",
"rowsAffected": "rows affected.",
"lastInsertId": "Last insert ID",
"loadTablesFailed": "Failed to load tables",
"loadTableDataFailed": "Failed to load table data",
"updateRowFailed": "Failed to update row",
"deleteRowFailed": "Failed to delete row",
"insertRowFailed": "Failed to insert row",
"executeSqlFailed": "Failed to execute SQL",
"resetDatabaseFailed": "Failed to reset database",
"resetFailed": "Reset failed: please try again.",
"resetSuccess": "Database has been reset to its default state with empty tables (agents, agent_runs, app_settings)."
}
} }

View File

@@ -12,6 +12,7 @@
"name": "Claudia", "name": "Claudia",
"welcome": "欢迎使用 Claudia", "welcome": "欢迎使用 Claudia",
"tagline": "强大的 Claude Code 会话管理工具", "tagline": "强大的 Claude Code 会话管理工具",
"returnHome": "返回首页",
"loading": "加载中...", "loading": "加载中...",
"error": "错误", "error": "错误",
"success": "成功", "success": "成功",
@@ -96,7 +97,19 @@
"justNow": "刚刚", "justNow": "刚刚",
"minutesAgo": "{{count}} 分钟前", "minutesAgo": "{{count}} 分钟前",
"hoursAgo": "{{count}} 小时前", "hoursAgo": "{{count}} 小时前",
"daysAgo": "{{count}} 天前" "daysAgo": "{{count}} 天前",
"loadingOutput": "加载输出中...",
"waitingForOutput": "等待输出...",
"noOutput": "暂无输出",
"agentRunningNoOutput": "智能体正在运行,但尚未收到输出",
"failedToLoadSessionOutput": "加载会话输出失败",
"outputRefreshed": "输出已刷新",
"failedToRefreshOutput": "刷新输出失败",
"agentExecutionCompleted": "智能体执行已完成",
"agentExecutionCancelled": "智能体执行已取消",
"loadingAgentRun": "正在加载运行详情...",
"selectProjectFirst": "请先选择项目目录",
"selectDirectoryFailed": "选择目录失败:{{message}}"
}, },
"navigation": { "navigation": {
"projects": "Claude Code 项目", "projects": "Claude Code 项目",
@@ -135,7 +148,27 @@
"sessions": "会话", "sessions": "会话",
"noSessions": "未找到会话", "noSessions": "未找到会话",
"lastModified": "最近修改", "lastModified": "最近修改",
"sessionHistory": "会话历史" "sessionHistory": "会话历史",
"sessionCount": "共 {{count}} 个会话"
},
"sessions": {
"firstMessage": "首条消息:",
"hasTodo": "包含待办"
},
"runningSessions": {
"title": "运行中的 Claude 会话",
"countRunning": "{{count}} 个运行中)",
"running": "运行中",
"resume": "恢复",
"loadFailed": "加载运行中的会话失败"
},
"widgets": {
"terminal": { "title": "终端" },
"common": { "running": "运行中..." },
"bash": {
"commandFailed": "命令执行失败",
"commandCompleted": "命令执行完成"
}
}, },
"agents": { "agents": {
"title": "Agent 管理", "title": "Agent 管理",
@@ -202,6 +235,11 @@
"enterTask": "输入智能体的任务", "enterTask": "输入智能体的任务",
"hooks": "钩子", "hooks": "钩子",
"configureHooks": "配置钩子", "configureHooks": "配置钩子",
"configureHooksDesc": "配置在工具执行前、中、后运行的钩子。更改将立即保存。",
"projectSettings": "项目设置",
"localSettings": "本地设置",
"initializing": "正在初始化智能体...",
"exportToFile": "导出为 .claudia.json",
"fullscreen": "全屏", "fullscreen": "全屏",
"stop": "停止", "stop": "停止",
"selectProjectPathAndTask": "选择项目路径并输入任务以运行智能体", "selectProjectPathAndTask": "选择项目路径并输入任务以运行智能体",
@@ -306,7 +344,13 @@
"condition": "条件", "condition": "条件",
"matchesRegex": "匹配正则表达式", "matchesRegex": "匹配正则表达式",
"message": "消息", "message": "消息",
"enterShellCommand": "输入 shell 命令..." "enterShellCommand": "输入 shell 命令...",
"projectHooks": "项目钩子",
"localHooks": "本地钩子"
},
"projectSettings": {
"gitignoreLocalWarning": "本地设置文件未添加到 .gitignore",
"addToGitignore": "添加到 .gitignore"
}, },
"settings": { "settings": {
"title": "设置", "title": "设置",
@@ -369,6 +413,8 @@
"verboseOutputDesc": "显示完整的 bash 和命令输出", "verboseOutputDesc": "显示完整的 bash 和命令输出",
"chatRetention": "聊天记录保留期 (天)", "chatRetention": "聊天记录保留期 (天)",
"chatRetentionDesc": "本地保留聊天记录的时长默认30 天)", "chatRetentionDesc": "本地保留聊天记录的时长默认30 天)",
"chatRetentionPermanent": "已启用永久保存,聊天记录将不会被自动删除",
"permanentRetention": "永久保存",
"claudeCodeInstallation": "Claude Code 安装", "claudeCodeInstallation": "Claude Code 安装",
"choosePreferredInstallation": "选择您偏好的 Claude Code 安装。", "choosePreferredInstallation": "选择您偏好的 Claude Code 安装。",
"loadingAvailableInstallations": "正在加载可用安装...", "loadingAvailableInstallations": "正在加载可用安装...",
@@ -678,6 +724,9 @@
"checkpoint": { "checkpoint": {
"title": "检查点", "title": "检查点",
"createCheckpoint": "创建检查点", "createCheckpoint": "创建检查点",
"createCheckpointDesc": "保存会话当前状态,并可填写可选说明。",
"descriptionOptional": "描述(可选)",
"descriptionPlaceholder": "例如:大型重构前",
"restoreCheckpoint": "恢复检查点", "restoreCheckpoint": "恢复检查点",
"deleteCheckpoint": "删除检查点", "deleteCheckpoint": "删除检查点",
"checkpointName": "检查点名称", "checkpointName": "检查点名称",
@@ -688,6 +737,15 @@
"checkpointSettingsTitle": "检查点设置", "checkpointSettingsTitle": "检查点设置",
"experimentalFeature": "实验性功能", "experimentalFeature": "实验性功能",
"checkpointWarning": "检查点可能会影响目录结构或导致数据丢失。请谨慎使用。", "checkpointWarning": "检查点可能会影响目录结构或导致数据丢失。请谨慎使用。",
"current": "当前",
"restoreToThis": "恢复到此检查点",
"forkFromThis": "从此检查点分叉",
"compareWithAnother": "与另一个检查点比较",
"checkpointComparison": "检查点对比",
"modifiedFiles": "修改的文件",
"addedFiles": "新增的文件",
"deletedFiles": "删除的文件",
"noPrompt": "无提示",
"automaticCheckpoints": "自动检查点", "automaticCheckpoints": "自动检查点",
"automaticCheckpointsDesc": "根据所选策略自动创建检查点", "automaticCheckpointsDesc": "根据所选策略自动创建检查点",
"checkpointStrategy": "检查点策略", "checkpointStrategy": "检查点策略",
@@ -894,4 +952,60 @@
"warning": { "warning": {
"title": "警告" "title": "警告"
} }
,
"agentRun": {
"runNotFound": "未找到运行记录",
"loadFailed": "加载执行详情失败",
"executionStopped": "已由用户停止执行",
"live": "实时",
"stopFailed": "停止失败,可能已结束"
},
"tabs": {
"scrollLeft": "向左滚动标签",
"scrollRight": "向右滚动标签",
"browseProjectsShortcut": "浏览项目Ctrl+T",
"maximumTabsReached": "已达到最大标签数({{count}}/20"
},
"storageTab": {
"title": "数据库存储",
"sqlQuery": "SQL 查询",
"resetDbShort": "重置库",
"selectTable": "选择数据表",
"rows": "行",
"searchInTable": "在表中搜索...",
"newRow": "新增行",
"actions": "操作",
"pagination": {
"showing": "显示第 {{from}}-{{to}} 条,共 {{total}} 条",
"pageOf": "第 {{page}} / {{total}} 页"
},
"editRow": "编辑行",
"editRowDesc": "更新 {{table}} 表中此行的值。",
"primaryKey": "主键",
"type": "类型",
"notNull": "非空",
"default": "默认值",
"insert": "插入",
"newRowDesc": "向 {{table}} 表添加一行。",
"deleteRow": "删除行",
"deleteRowConfirm": "确定要删除此行吗?该操作无法撤销。",
"resetDatabaseTitle": "重置数据库",
"resetDatabaseDesc": "这将删除所有数据并使用默认结构重新创建数据库agents、agent_runs、app_settings 为空表)。数据库将恢复到首次安装应用时的状态。该操作无法撤销。",
"resetWarning": "您所有的智能体、运行记录和设置都将被永久删除!",
"sqlEditorTitle": "SQL 查询编辑器",
"sqlEditorDesc": "在数据库上执行原始 SQL 语句,请谨慎使用。",
"sqlQueryPlaceholder": "SELECT * FROM agents LIMIT 10;",
"queryExecuted": "查询执行成功。",
"rowsAffected": "行受影响。",
"lastInsertId": "最后插入 ID",
"loadTablesFailed": "加载数据表失败",
"loadTableDataFailed": "加载数据失败",
"updateRowFailed": "更新行失败",
"deleteRowFailed": "删除行失败",
"insertRowFailed": "插入行失败",
"executeSqlFailed": "执行 SQL 失败",
"resetDatabaseFailed": "重置数据库失败",
"resetFailed": "重置失败,请重试。",
"resetSuccess": "数据库已重置为默认状态agents、agent_runs、app_settings 为空表)。"
}
} }