增加永久存储记录信息
完善 i18n
This commit is contained in:
29
src/App.tsx
29
src/App.tsx
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, lazy, Suspense } from "react";
|
||||
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 { OutputCacheProvider } from "@/lib/outputCache";
|
||||
import { TabProvider } from "@/contexts/TabContext";
|
||||
@@ -98,6 +98,13 @@ function AppContent() {
|
||||
initializeBackendLanguage();
|
||||
}, []); // 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
|
||||
useEffect(() => {
|
||||
if (view === "projects" && projects.length > 0 && !hasTrackedFirstChat) {
|
||||
@@ -427,32 +434,16 @@ function AppContent() {
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
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 */}
|
||||
<RunningClaudeSessions />
|
||||
|
||||
{/* Project list */}
|
||||
{/* Project list with integrated new session button */}
|
||||
{projects.length > 0 ? (
|
||||
<ProjectList
|
||||
projects={projects}
|
||||
onProjectClick={handleProjectClick}
|
||||
onProjectSettings={handleProjectSettings}
|
||||
onNewSession={handleNewSession}
|
||||
loading={loading}
|
||||
className="animate-fade-in"
|
||||
/>
|
||||
|
||||
@@ -274,7 +274,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: "Select Project Directory"
|
||||
title: t('webview.selectProjectDirectory')
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
@@ -576,7 +576,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">{t('agents.execute')}: {agent.name}</h1>
|
||||
<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>
|
||||
</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>
|
||||
<span>Claude 4 Sonnet</span>
|
||||
<span>{t('agents.sonnetName')}</span>
|
||||
</div>
|
||||
</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>
|
||||
<span>Claude 4.1 Opus</span>
|
||||
<span>{t('agents.opusName')}</span>
|
||||
</div>
|
||||
</button>
|
||||
</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 gap-3">
|
||||
<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>
|
||||
)}
|
||||
@@ -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 gap-2">
|
||||
{renderIcon()}
|
||||
<h2 className="text-lg font-semibold">{agent.name} - Output</h2>
|
||||
<h2 className="text-lg font-semibold">{agent.name} - {t('app.output')}</h2>
|
||||
{isRunning && (
|
||||
<div className="flex items-center gap-1">
|
||||
<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>
|
||||
@@ -878,7 +878,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Close
|
||||
{t('app.close')}
|
||||
</Button>
|
||||
</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 gap-3">
|
||||
<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>
|
||||
)}
|
||||
@@ -967,8 +967,8 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
|
||||
<Tabs value={activeHooksTab} onValueChange={setActiveHooksTab} className="flex-1 flex flex-col overflow-hidden">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="project">Project Settings</TabsTrigger>
|
||||
<TabsTrigger value="local">Local Settings</TabsTrigger>
|
||||
<TabsTrigger value="project">{t('agents.projectSettings') || 'Project Settings'}</TabsTrigger>
|
||||
<TabsTrigger value="local">{t('agents.localSettings') || 'Local Settings'}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="project" className="flex-1 overflow-auto">
|
||||
|
||||
@@ -237,7 +237,7 @@ export function AgentRunOutputViewer({
|
||||
}
|
||||
} catch (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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -286,12 +286,12 @@ export function AgentRunOutputViewer({
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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];
|
||||
@@ -305,7 +305,7 @@ export function AgentRunOutputViewer({
|
||||
const jsonl = rawJsonlOutput.join('\n');
|
||||
await navigator.clipboard.writeText(jsonl);
|
||||
setCopyPopoverOpen(false);
|
||||
setToast({ message: 'Output copied as JSONL', type: 'success' });
|
||||
setToast({ message: t('webview.sessionOutputCopiedJsonl'), type: 'success' });
|
||||
};
|
||||
|
||||
const handleCopyAsMarkdown = async () => {
|
||||
@@ -363,7 +363,7 @@ export function AgentRunOutputViewer({
|
||||
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
setCopyPopoverOpen(false);
|
||||
setToast({ message: 'Output copied as Markdown', type: 'success' });
|
||||
setToast({ message: t('webview.sessionOutputCopiedMarkdown'), type: 'success' });
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
@@ -383,7 +383,7 @@ export function AgentRunOutputViewer({
|
||||
const success = await api.killAgentSession(run.id);
|
||||
|
||||
if (success) {
|
||||
setToast({ message: 'Agent execution stopped', type: 'success' });
|
||||
setToast({ message: t('agentRun.executionStopped'), type: 'success' });
|
||||
|
||||
// Clean up listeners
|
||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||
@@ -410,14 +410,11 @@ export function AgentRunOutputViewer({
|
||||
// Refresh the output to get updated status
|
||||
await loadOutput(true);
|
||||
} else {
|
||||
setToast({ message: 'Failed to stop agent - it may have already finished', type: 'error' });
|
||||
setToast({ message: t('agentRun.stopFailed'), type: 'error' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[AgentRunOutputViewer] Failed to stop agent:', err);
|
||||
setToast({
|
||||
message: `Failed to stop execution: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
||||
type: 'error'
|
||||
});
|
||||
setToast({ message: t('agentRun.stopFailed'), type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -515,7 +512,7 @@ export function AgentRunOutputViewer({
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<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>
|
||||
);
|
||||
@@ -537,7 +534,7 @@ export function AgentRunOutputViewer({
|
||||
{run.status === 'running' && (
|
||||
<div className="flex items-center gap-1">
|
||||
<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>
|
||||
)}
|
||||
</CardTitle>
|
||||
@@ -546,7 +543,7 @@ export function AgentRunOutputViewer({
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-2">
|
||||
<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>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
@@ -579,7 +576,7 @@ export function AgentRunOutputViewer({
|
||||
className="h-8 px-2"
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{t('app.copyOutput')}
|
||||
<ChevronDown className="h-3 w-3 ml-1" />
|
||||
</Button>
|
||||
}
|
||||
@@ -611,7 +608,7 @@ export function AgentRunOutputViewer({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
||||
title={isFullscreen ? t('webview.exitFullScreen') : t('webview.enterFullScreen')}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
@@ -625,7 +622,7 @@ export function AgentRunOutputViewer({
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
title="Refresh output"
|
||||
title={t('app.refresh')}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
<RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
@@ -636,7 +633,7 @@ export function AgentRunOutputViewer({
|
||||
size="sm"
|
||||
onClick={handleStop}
|
||||
disabled={refreshing}
|
||||
title="Stop execution"
|
||||
title={t('agents.stop')}
|
||||
className="h-8 px-2 text-destructive hover:text-destructive"
|
||||
>
|
||||
<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 space-x-2">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span>Loading output...</span>
|
||||
<span>{t('app.loadingOutput')}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<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
|
||||
@@ -745,7 +742,7 @@ export function AgentRunOutputViewer({
|
||||
disabled={refreshing}
|
||||
>
|
||||
<StopCircle className="h-4 w-4 mr-2" />
|
||||
Stop
|
||||
{t('agents.stop')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -754,7 +751,7 @@ export function AgentRunOutputViewer({
|
||||
onClick={() => setIsFullscreen(false)}
|
||||
>
|
||||
<Minimize2 className="h-4 w-4 mr-2" />
|
||||
Exit Fullscreen
|
||||
{t('webview.exitFullScreen')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -766,7 +763,7 @@ export function AgentRunOutputViewer({
|
||||
<div className="max-w-4xl mx-auto space-y-2">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
No output available yet
|
||||
{t('app.noOutput')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -103,7 +103,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load run:", err);
|
||||
setError("Failed to load execution details");
|
||||
setError(t('agentRun.loadFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -198,7 +198,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
|
||||
type: "result",
|
||||
subtype: "error",
|
||||
is_error: true,
|
||||
result: "Execution stopped by user",
|
||||
result: t('agentRun.executionStopped'),
|
||||
duration_ms: 0,
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
@@ -235,8 +235,8 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
|
||||
if (error || !run) {
|
||||
return (
|
||||
<div className={cn("flex flex-col items-center justify-center h-full", className)}>
|
||||
<p className="text-destructive mb-4">{error || "Run not found"}</p>
|
||||
<Button onClick={onBack}>Go Back</Button>
|
||||
<p className="text-destructive mb-4">{error || t('agentRun.runNotFound')}</p>
|
||||
<Button onClick={onBack}>{t('app.back')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -264,7 +264,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
|
||||
{renderIcon(run.agent_icon)}
|
||||
<div>
|
||||
<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>
|
||||
@@ -278,7 +278,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<StopCircle className="h-4 w-4 mr-1" />
|
||||
Stop
|
||||
{t('agents.stop')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -326,10 +326,10 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -349,7 +349,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
|
||||
{run.metrics?.total_tokens && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Hash className="h-3 w-3" />
|
||||
<span>{run.metrics.total_tokens} tokens</span>
|
||||
<span>{run.metrics.total_tokens} {t('usage.tokens')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
||||
variant="ghost"
|
||||
onClick={() => handleExportAgent(agent)}
|
||||
className="flex items-center gap-1"
|
||||
title="Export agent to .claudia.json"
|
||||
title={t('agents.exportToFile')}
|
||||
>
|
||||
<Upload className="h-3 w-3" />
|
||||
{t('agents.export')}
|
||||
|
||||
@@ -634,7 +634,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: "Select Project Directory"
|
||||
title: t('webview.selectProjectDirectory')
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
@@ -644,7 +644,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
} catch (err) {
|
||||
console.error("Failed to select directory:", 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 });
|
||||
|
||||
if (!projectPath) {
|
||||
setError("Please select a project directory first");
|
||||
setError(t('app.selectProjectFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { Component, ReactNode } from "react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import i18n from "@/lib/i18n";
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
@@ -51,14 +52,14 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
||||
<div className="flex items-start gap-4">
|
||||
<AlertCircle className="h-8 w-8 text-destructive flex-shrink-0 mt-0.5" />
|
||||
<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">
|
||||
An error occurred while rendering this component.
|
||||
{i18n.t('errorBoundary.errorOccurred')}
|
||||
</p>
|
||||
{this.state.error.message && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-sm cursor-pointer text-muted-foreground hover:text-foreground">
|
||||
Error details
|
||||
{i18n.t('errorBoundary.errorDetails')}
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-muted p-2 rounded overflow-auto">
|
||||
{this.state.error.message}
|
||||
@@ -70,7 +71,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
||||
size="sm"
|
||||
className="mt-4"
|
||||
>
|
||||
Try again
|
||||
{i18n.t('errorBoundary.tryAgain')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import asteriskLogo from "@/assets/nfo/asterisk-logo.png";
|
||||
import keygennMusic from "@/assets/nfo/claudia-nfo.ogg";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface NFOCreditsProps {
|
||||
/**
|
||||
@@ -22,6 +23,7 @@ interface NFOCreditsProps {
|
||||
* <NFOCredits onClose={() => setShowNFO(false)} />
|
||||
*/
|
||||
export const NFOCredits: React.FC<NFOCreditsProps> = ({ onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
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");
|
||||
}}
|
||||
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" />
|
||||
<span className="text-xs">File a bug</span>
|
||||
<span className="text-xs">{t('app.fileABug')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
Settings,
|
||||
MoreVertical,
|
||||
Search,
|
||||
X
|
||||
X,
|
||||
Plus
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
@@ -39,6 +40,10 @@ interface ProjectListProps {
|
||||
* Callback when hooks configuration is clicked
|
||||
*/
|
||||
onProjectSettings?: (project: Project) => void;
|
||||
/**
|
||||
* Callback when new session button is clicked
|
||||
*/
|
||||
onNewSession?: () => void;
|
||||
/**
|
||||
* Whether the list is currently loading
|
||||
*/
|
||||
@@ -72,6 +77,7 @@ export const ProjectList: React.FC<ProjectListProps> = ({
|
||||
projects,
|
||||
onProjectClick,
|
||||
onProjectSettings,
|
||||
onNewSession,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -107,16 +113,32 @@ export const ProjectList: React.FC<ProjectListProps> = ({
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* Search bar and results info */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
{/* Action bar with new session button and search */}
|
||||
<div className="flex flex-col lg:flex-row gap-3 items-stretch lg:items-center justify-between">
|
||||
{/* 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" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t('searchProjects')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 pr-9"
|
||||
className="pl-9 pr-9 h-10"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
@@ -130,8 +152,8 @@ export const ProjectList: React.FC<ProjectListProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Results info */}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{/* Results info */}
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{searchQuery ? (
|
||||
<span>
|
||||
{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>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HooksEditor } from '@/components/HooksEditor';
|
||||
import { SlashCommandsManager } from '@/components/SlashCommandsManager';
|
||||
import { api } from '@/lib/api';
|
||||
@@ -33,6 +34,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
onBack,
|
||||
className
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('commands');
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
||||
|
||||
@@ -89,7 +91,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
@@ -109,15 +111,15 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="commands" className="gap-2">
|
||||
<Command className="h-4 w-4" />
|
||||
Slash Commands
|
||||
{t('slashCommands.slashCommands')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="project" className="gap-2">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
Project Hooks
|
||||
{t('hooks.projectHooks', 'Project Hooks')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="local" className="gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
Local Hooks
|
||||
{t('hooks.localHooks', 'Local Hooks')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -125,7 +127,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
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>
|
||||
@@ -145,7 +147,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
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>
|
||||
@@ -165,7 +167,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
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>
|
||||
@@ -177,7 +179,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-600" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-yellow-600">
|
||||
Local settings file is not in .gitignore
|
||||
{t('projectSettings.gitignoreLocalWarning')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -185,7 +187,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
variant="outline"
|
||||
onClick={addToGitIgnore}
|
||||
>
|
||||
Add to .gitignore
|
||||
{t('projectSettings.addToGitignore')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error("Failed to load running sessions:", err);
|
||||
setError("Failed to load running sessions");
|
||||
setError(t('runningSessions.loadFailed'));
|
||||
} finally {
|
||||
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-1">
|
||||
<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>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({runningSessions.length} running)
|
||||
{t('runningSessions.countRunning', { count: runningSessions.length })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,7 @@ export const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({
|
||||
{sessionId.substring(0, 20)}...
|
||||
</p>
|
||||
<span className="text-xs text-green-600 font-medium">
|
||||
Running
|
||||
{t('runningSessions.running')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@ export const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Play className="h-3 w-3 mr-1" />
|
||||
Resume
|
||||
{t('runningSessions.resume')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ClaudeMemoriesDropdown } from "@/components/ClaudeMemoriesDropdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatUnixTimestamp, formatISOTimestamp, truncateText, getFirstLine } from "@/lib/date-utils";
|
||||
import type { Session, ClaudeMdFile } from "@/lib/api";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface SessionListProps {
|
||||
/**
|
||||
@@ -57,6 +58,7 @@ export const SessionList: React.FC<SessionListProps> = ({
|
||||
onEditClaudeFile,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// Calculate pagination
|
||||
@@ -89,7 +91,7 @@ export const SessionList: React.FC<SessionListProps> = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-base font-medium truncate">{projectPath}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{sessions.length} session{sessions.length !== 1 ? 's' : ''}
|
||||
{t('projects.sessionCount', { count: sessions.length })}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -149,7 +151,7 @@ export const SessionList: React.FC<SessionListProps> = ({
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
<span>First message:</span>
|
||||
<span>{t('sessions.firstMessage')}</span>
|
||||
</div>
|
||||
<p className="text-xs line-clamp-2 text-foreground/80">
|
||||
{truncateText(getFirstLine(session.first_message), 100)}
|
||||
@@ -173,7 +175,7 @@ export const SessionList: React.FC<SessionListProps> = ({
|
||||
{session.todo_data && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>Has todo</span>
|
||||
<span>{t('sessions.hasTodo')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -189,7 +189,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
}
|
||||
} catch (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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -223,12 +223,12 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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];
|
||||
@@ -242,7 +242,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
const jsonl = rawJsonlOutput.join('\n');
|
||||
await navigator.clipboard.writeText(jsonl);
|
||||
setCopyPopoverOpen(false);
|
||||
setToast({ message: 'Output copied as JSONL', type: 'success' });
|
||||
setToast({ message: t('webview.sessionOutputCopiedJsonl'), type: 'success' });
|
||||
};
|
||||
|
||||
const handleCopyAsMarkdown = async () => {
|
||||
@@ -297,7 +297,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
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);
|
||||
try {
|
||||
await loadOutput(true); // Skip cache when manually refreshing
|
||||
setToast({ message: 'Output refreshed', type: 'success' });
|
||||
setToast({ message: t('app.outputRefreshed'), type: 'success' });
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh output:', error);
|
||||
setToast({ message: 'Failed to refresh output', type: 'error' });
|
||||
setToast({ message: t('app.failedToRefreshOutput'), type: 'error' });
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
@@ -388,7 +388,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-2xl">{session.agent_icon}</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">
|
||||
<Badge variant={session.status === 'running' ? 'default' : 'secondary'}>
|
||||
{session.status}
|
||||
@@ -396,7 +396,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
{session.status === 'running' && (
|
||||
<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>
|
||||
Live
|
||||
{t('agentRun.live')}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -459,7 +459,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
size="sm"
|
||||
onClick={refreshOutput}
|
||||
disabled={refreshing}
|
||||
title="Refresh output"
|
||||
title={t('app.refresh')}
|
||||
>
|
||||
<RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
</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 space-x-2">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span>Loading output...</span>
|
||||
<span>{t('app.loadingOutput')}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -498,14 +498,14 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
{session.status === 'running' ? (
|
||||
<>
|
||||
<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">
|
||||
Agent is running but no output received yet
|
||||
{t('app.agentRunningNoOutput')}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-muted-foreground">No output available</p>
|
||||
<p className="text-muted-foreground">{t('app.noOutput')}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -514,7 +514,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
disabled={refreshing}
|
||||
>
|
||||
{refreshing ? <RefreshCw className="h-4 w-4 animate-spin mr-2" /> : <RotateCcw className="h-4 w-4 mr-2" />}
|
||||
Refresh
|
||||
{t('app.refresh')}
|
||||
</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 gap-2">
|
||||
<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' && (
|
||||
<div className="flex items-center gap-1">
|
||||
<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>
|
||||
@@ -605,7 +605,7 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Close
|
||||
{t('app.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -632,14 +632,14 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
{session.status === 'running' ? (
|
||||
<>
|
||||
<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">
|
||||
Agent is running but no output received yet
|
||||
{t('app.agentRunningNoOutput')}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-muted-foreground">No output available</p>
|
||||
<p className="text-muted-foreground">{t('app.noOutput')}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -658,19 +658,37 @@ export const Settings: React.FC<SettingsProps> = ({
|
||||
{/* Cleanup Period */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cleanup">{t('settings.generalOptions.chatRetention')}</Label>
|
||||
<Input
|
||||
id="cleanup"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="30"
|
||||
value={settings?.cleanupPeriodDays || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value ? parseInt(e.target.value) : undefined;
|
||||
updateSetting("cleanupPeriodDays", value);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
id="cleanup"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="30"
|
||||
value={settings?.cleanupPeriodDays === -1 ? "" : (settings?.cleanupPeriodDays || "")}
|
||||
onChange={(e) => {
|
||||
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">
|
||||
{t('settings.generalOptions.chatRetentionDesc')}
|
||||
{settings?.cleanupPeriodDays === -1
|
||||
? t('settings.generalOptions.chatRetentionPermanent')
|
||||
: t('settings.generalOptions.chatRetentionDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export const StorageTab: React.FC = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load tables:", err);
|
||||
setError("Failed to load tables");
|
||||
setError(t('storageTab.loadTablesFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -158,7 +158,7 @@ export const StorageTab: React.FC = () => {
|
||||
setCurrentPage(page);
|
||||
} catch (err) {
|
||||
console.error("Failed to load table data:", err);
|
||||
setError("Failed to load table data");
|
||||
setError(t('storageTab.loadTableDataFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -205,7 +205,7 @@ export const StorageTab: React.FC = () => {
|
||||
setEditingRow(null);
|
||||
} catch (err) {
|
||||
console.error("Failed to update row:", err);
|
||||
setError("Failed to update row");
|
||||
setError(t('storageTab.updateRowFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export const StorageTab: React.FC = () => {
|
||||
setDeletingRow(null);
|
||||
} catch (err) {
|
||||
console.error("Failed to delete row:", err);
|
||||
setError("Failed to delete row");
|
||||
setError(t('storageTab.deleteRowFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -244,7 +244,7 @@ export const StorageTab: React.FC = () => {
|
||||
setNewRow(null);
|
||||
} catch (err) {
|
||||
console.error("Failed to insert row:", err);
|
||||
setError("Failed to insert row");
|
||||
setError(t('storageTab.insertRowFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -269,7 +269,7 @@ export const StorageTab: React.FC = () => {
|
||||
}
|
||||
} catch (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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -287,14 +287,14 @@ export const StorageTab: React.FC = () => {
|
||||
setTableData(null);
|
||||
setShowResetConfirm(false);
|
||||
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",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to reset database:", err);
|
||||
setError("Failed to reset database");
|
||||
setError(t('storageTab.resetDatabaseFailed'));
|
||||
setToast({
|
||||
message: "Reset Failed: Failed to reset the database. Please try again.",
|
||||
message: t('storageTab.resetFailed'),
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
@@ -337,7 +337,7 @@ export const StorageTab: React.FC = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<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 className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -347,7 +347,7 @@ export const StorageTab: React.FC = () => {
|
||||
className="gap-2 h-8 text-xs"
|
||||
>
|
||||
<Terminal className="h-3 w-3" />
|
||||
SQL Query
|
||||
{t('storageTab.sqlQuery')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -356,7 +356,7 @@ export const StorageTab: React.FC = () => {
|
||||
className="gap-2 h-8 text-xs"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Reset DB
|
||||
{t('storageTab.resetDbShort')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,7 +365,7 @@ export const StorageTab: React.FC = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={selectedTable} onValueChange={setSelectedTable}>
|
||||
<SelectTrigger className="w-[200px] h-8 text-xs">
|
||||
<SelectValue placeholder="Select a table">
|
||||
<SelectValue placeholder={t('storageTab.selectTable')}>
|
||||
{selectedTable && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Table className="h-3 w-3" />
|
||||
@@ -380,7 +380,7 @@ export const StorageTab: React.FC = () => {
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{table.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground ml-2">
|
||||
{table.row_count} rows
|
||||
{table.row_count} {t('storageTab.rows')}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -391,7 +391,7 @@ export const StorageTab: React.FC = () => {
|
||||
<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" />
|
||||
<Input
|
||||
placeholder="Search in table..."
|
||||
placeholder={t('storageTab.searchInTable')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-8 h-8 text-xs"
|
||||
@@ -406,7 +406,7 @@ export const StorageTab: React.FC = () => {
|
||||
className="gap-2 h-8 text-xs"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
New Row
|
||||
{t('storageTab.newRow')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -437,7 +437,7 @@ export const StorageTab: React.FC = () => {
|
||||
</th>
|
||||
))}
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-muted-foreground">
|
||||
Actions
|
||||
{t('storageTab.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -520,9 +520,11 @@ export const StorageTab: React.FC = () => {
|
||||
{tableData.total_pages > 1 && (
|
||||
<div className="flex items-center justify-between p-3 border-t">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Showing {(currentPage - 1) * pageSize + 1} to{" "}
|
||||
{Math.min(currentPage * pageSize, tableData.total_rows)} of{" "}
|
||||
{tableData.total_rows} rows
|
||||
{t('storageTab.pagination.showing', {
|
||||
from: (currentPage - 1) * pageSize + 1,
|
||||
to: Math.min(currentPage * pageSize, tableData.total_rows),
|
||||
total: tableData.total_rows
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -533,10 +535,10 @@ export const StorageTab: React.FC = () => {
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<ChevronLeft className="h-3 w-3" />
|
||||
Previous
|
||||
{t('app.previous')}
|
||||
</Button>
|
||||
<div className="text-xs">
|
||||
Page {currentPage} of {tableData.total_pages}
|
||||
{t('storageTab.pagination.pageOf', { page: currentPage, total: tableData.total_pages })}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -545,7 +547,7 @@ export const StorageTab: React.FC = () => {
|
||||
disabled={currentPage === tableData.total_pages}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
Next
|
||||
{t('app.next')}
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -575,9 +577,9 @@ export const StorageTab: React.FC = () => {
|
||||
<Dialog open={!!editingRow} onOpenChange={() => setEditingRow(null)}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Row</DialogTitle>
|
||||
<DialogTitle>{t('storageTab.editRow')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the values for this row in the {selectedTable} table.
|
||||
{t('storageTab.editRowDesc', { table: selectedTable })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editingRow && tableData && (
|
||||
@@ -588,7 +590,7 @@ export const StorageTab: React.FC = () => {
|
||||
{column.name}
|
||||
{column.pk && (
|
||||
<span className="text-xs text-muted-foreground ml-2">
|
||||
(Primary Key)
|
||||
({t('storageTab.primaryKey')})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
@@ -622,9 +624,9 @@ export const StorageTab: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Type: {column.type_name}
|
||||
{column.notnull && ", NOT NULL"}
|
||||
{column.dflt_value && `, Default: ${column.dflt_value}`}
|
||||
{t('storageTab.type')}: {column.type_name}
|
||||
{column.notnull && `, ${t('storageTab.notNull')}`}
|
||||
{column.dflt_value && `, ${t('storageTab.default')}: ${column.dflt_value}`}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -632,7 +634,7 @@ export const StorageTab: React.FC = () => {
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditingRow(null)}>
|
||||
Cancel
|
||||
{t('app.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleUpdateRow(editingRow!)}
|
||||
@@ -641,7 +643,7 @@ export const StorageTab: React.FC = () => {
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Update"
|
||||
t('app.update')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -652,9 +654,9 @@ export const StorageTab: React.FC = () => {
|
||||
<Dialog open={!!newRow} onOpenChange={() => setNewRow(null)}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Row</DialogTitle>
|
||||
<DialogTitle>{t('storageTab.newRow')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new row to the {selectedTable} table.
|
||||
{t('storageTab.newRowDesc', { table: selectedTable })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{newRow && tableData && (
|
||||
@@ -665,7 +667,7 @@ export const StorageTab: React.FC = () => {
|
||||
{column.name}
|
||||
{column.notnull && (
|
||||
<span className="text-xs text-destructive ml-2">
|
||||
(Required)
|
||||
({t('validation.required')})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
@@ -697,8 +699,8 @@ export const StorageTab: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Type: {column.type_name}
|
||||
{column.dflt_value && `, Default: ${column.dflt_value}`}
|
||||
{t('storageTab.type')}: {column.type_name}
|
||||
{column.dflt_value && `, ${t('storageTab.default')}: ${column.dflt_value}`}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -706,7 +708,7 @@ export const StorageTab: React.FC = () => {
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setNewRow(null)}>
|
||||
Cancel
|
||||
{t('app.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleInsertRow(newRow!)}
|
||||
@@ -715,7 +717,7 @@ export const StorageTab: React.FC = () => {
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Insert"
|
||||
t('storageTab.insert')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -726,10 +728,9 @@ export const StorageTab: React.FC = () => {
|
||||
<Dialog open={!!deletingRow} onOpenChange={() => setDeletingRow(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Row</DialogTitle>
|
||||
<DialogTitle>{t('storageTab.deleteRow')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this row? This action cannot be
|
||||
undone.
|
||||
{t('storageTab.deleteRowConfirm')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{deletingRow && (
|
||||
@@ -752,7 +753,7 @@ export const StorageTab: React.FC = () => {
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeletingRow(null)}>
|
||||
Cancel
|
||||
{t('app.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -762,7 +763,7 @@ export const StorageTab: React.FC = () => {
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Delete"
|
||||
t('app.delete')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -773,18 +774,15 @@ export const StorageTab: React.FC = () => {
|
||||
<Dialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset Database</DialogTitle>
|
||||
<DialogTitle>{t('storageTab.resetDatabaseTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
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.
|
||||
{t('storageTab.resetDatabaseDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center gap-3 p-4 rounded-md bg-destructive/10 text-destructive">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">
|
||||
All your agents, runs, and settings will be permanently deleted!
|
||||
{t('storageTab.resetWarning')}
|
||||
</span>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -792,7 +790,7 @@ export const StorageTab: React.FC = () => {
|
||||
variant="outline"
|
||||
onClick={() => setShowResetConfirm(false)}
|
||||
>
|
||||
Cancel
|
||||
{t('app.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -802,7 +800,7 @@ export const StorageTab: React.FC = () => {
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Reset Database"
|
||||
t('storageTab.resetDatabaseTitle')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -813,19 +811,19 @@ export const StorageTab: React.FC = () => {
|
||||
<Dialog open={showSqlEditor} onOpenChange={setShowSqlEditor}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>SQL Query Editor</DialogTitle>
|
||||
<DialogTitle>{t('storageTab.sqlEditorTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Execute raw SQL queries on the database. Use with caution.
|
||||
{t('storageTab.sqlEditorDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sql-query">SQL Query</Label>
|
||||
<Label htmlFor="sql-query">{t('storageTab.sqlQuery')}</Label>
|
||||
<Textarea
|
||||
id="sql-query"
|
||||
value={sqlQuery}
|
||||
onChange={(e) => setSqlQuery(e.target.value)}
|
||||
placeholder="SELECT * FROM agents LIMIT 10;"
|
||||
placeholder={t('storageTab.sqlQueryPlaceholder')}
|
||||
className="font-mono text-sm h-32"
|
||||
/>
|
||||
</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="flex items-center gap-2">
|
||||
<Check className="h-4 w-4" />
|
||||
Query executed successfully. {sqlResult.rows_affected} rows
|
||||
affected.
|
||||
{t('storageTab.queryExecuted')} {sqlResult.rows_affected} {t('storageTab.rowsAffected')}
|
||||
{sqlResult.last_insert_rowid && (
|
||||
<span>
|
||||
Last insert ID: {sqlResult.last_insert_rowid}
|
||||
{t('storageTab.lastInsertId')}: {sqlResult.last_insert_rowid}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -927,7 +924,7 @@ export const StorageTab: React.FC = () => {
|
||||
setSqlError(null);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
{t('app.close')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExecuteSql}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTabState } from '@/hooks/useTabState';
|
||||
import { Tab, useTabContext } from '@/contexts/TabContext';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTrackEvent } from '@/hooks';
|
||||
import { useTranslation as useAppTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface TabItemProps {
|
||||
tab: Tab;
|
||||
@@ -16,6 +17,7 @@ interface TabItemProps {
|
||||
}
|
||||
|
||||
const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDragging = false, setDraggedTabId }) => {
|
||||
const { t } = useAppTranslation();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const getIcon = () => {
|
||||
@@ -103,7 +105,7 @@ const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDr
|
||||
{tab.hasUnsavedChanges && !statusIcon && (
|
||||
<span
|
||||
className="w-1.5 h-1.5 bg-primary rounded-full"
|
||||
title="Unsaved changes"
|
||||
title={t('app.unsavedChanges')}
|
||||
/>
|
||||
)}
|
||||
</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",
|
||||
(isHovered || isActive) ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
title={`Close ${tab.title}`}
|
||||
title={`${t('app.close')} ${tab.title}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
@@ -135,6 +137,7 @@ interface TabManagerProps {
|
||||
}
|
||||
|
||||
export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
const { t } = useAppTranslation();
|
||||
const {
|
||||
tabs,
|
||||
activeTabId,
|
||||
@@ -342,7 +345,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
"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"
|
||||
)}
|
||||
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">
|
||||
<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",
|
||||
"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">
|
||||
<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"
|
||||
: "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" />
|
||||
</button>
|
||||
|
||||
@@ -294,7 +294,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{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">
|
||||
{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">
|
||||
{node.checkpoint.metadata.userPrompt || "No prompt"}
|
||||
{node.checkpoint.metadata.userPrompt || t('checkpoint.noPrompt') || 'No prompt'}
|
||||
</p>
|
||||
|
||||
<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" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Restore to this checkpoint</TooltipContent>
|
||||
<TooltipContent>{t('checkpoint.restoreToThis') || 'Restore to this checkpoint'}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -360,7 +360,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
<GitFork className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Fork from this checkpoint</TooltipContent>
|
||||
<TooltipContent>{t('checkpoint.forkFromThis') || 'Fork from this checkpoint'}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -379,7 +379,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
<Diff className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Compare with another checkpoint</TooltipContent>
|
||||
<TooltipContent>{t('checkpoint.compareWithAnother') || 'Compare with another checkpoint'}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
@@ -442,7 +442,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Save className="h-3 w-3 mr-1" />
|
||||
Checkpoint
|
||||
{t('checkpoint.createCheckpoint')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -469,18 +469,18 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Checkpoint</DialogTitle>
|
||||
<DialogTitle>{t('checkpoint.createCheckpoint')}</DialogTitle>
|
||||
<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>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description (optional)</Label>
|
||||
<Label htmlFor="description">{t('checkpoint.descriptionOptional') || 'Description (optional)'}</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="e.g., Before major refactoring"
|
||||
placeholder={t('checkpoint.descriptionPlaceholder') || 'e.g., Before major refactoring'}
|
||||
value={checkpointDescription}
|
||||
onChange={(e) => setCheckpointDescription(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
@@ -498,13 +498,13 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
onClick={() => setShowCreateDialog(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
{t('app.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateCheckpoint}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Create Checkpoint
|
||||
{t('checkpoint.createCheckpoint')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -514,7 +514,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
<Dialog open={showDiffDialog} onOpenChange={setShowDiffDialog}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Checkpoint Comparison</DialogTitle>
|
||||
<DialogTitle>{t('checkpoint.checkpointComparison') || 'Checkpoint Comparison'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Changes between "{selectedCheckpoint?.description || selectedCheckpoint?.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">
|
||||
<Card>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -548,14 +548,14 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
{/* Token delta */}
|
||||
<div className="flex items-center justify-center">
|
||||
<Badge variant={diff.tokenDelta > 0 ? "default" : "secondary"}>
|
||||
{diff.tokenDelta > 0 ? "+" : ""}{diff.tokenDelta.toLocaleString()} tokens
|
||||
{diff.tokenDelta > 0 ? "+" : ""}{diff.tokenDelta.toLocaleString()} {t('usage.tokens')}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* File lists */}
|
||||
{diff.modifiedFiles.length > 0 && (
|
||||
<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">
|
||||
{diff.modifiedFiles.map((file) => (
|
||||
<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 && (
|
||||
<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">
|
||||
{diff.addedFiles.map((file) => (
|
||||
<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 && (
|
||||
<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">
|
||||
{diff.deletedFiles.map((file) => (
|
||||
<div key={file} className="text-xs font-mono text-red-600">
|
||||
@@ -607,7 +607,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
setCompareCheckpoint(null);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
{t('app.close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -635,6 +635,7 @@ export const BashWidget: React.FC<{
|
||||
description?: string;
|
||||
result?: any;
|
||||
}> = ({ command, description, result }) => {
|
||||
const { t } = useTranslation();
|
||||
// Extract result content if available
|
||||
let resultContent = '';
|
||||
let isError = false;
|
||||
@@ -660,7 +661,7 @@ export const BashWidget: React.FC<{
|
||||
<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">
|
||||
<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 && (
|
||||
<>
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||
@@ -671,7 +672,7 @@ export const BashWidget: React.FC<{
|
||||
{!result && (
|
||||
<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" />
|
||||
<span>Running...</span>
|
||||
<span>{t('widgets.common.running')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -688,7 +689,7 @@ export const BashWidget: React.FC<{
|
||||
? "border-red-500/20 bg-red-500/5 text-red-400"
|
||||
: "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>
|
||||
|
||||
@@ -112,7 +112,7 @@ export const Topbar: React.FC<TopbarProps> = ({
|
||||
// Emit event to return to home
|
||||
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">
|
||||
<Circle
|
||||
|
||||
@@ -172,7 +172,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
className={cn("flex flex-col h-full bg-background border-l", className)}
|
||||
tabIndex={-1}
|
||||
role="region"
|
||||
aria-label="Web preview"
|
||||
aria-label={t('webview.preview')}
|
||||
>
|
||||
{/* Browser Top Bar */}
|
||||
<div className="border-b bg-muted/30 flex-shrink-0">
|
||||
@@ -322,7 +322,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
ref={iframeRef}
|
||||
src={currentUrl}
|
||||
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"
|
||||
onLoad={() => setIsLoading(false)}
|
||||
onError={() => {
|
||||
|
||||
@@ -90,7 +90,7 @@ export const SessionHeader: React.FC<SessionHeaderProps> = React.memo(({
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
Select Project
|
||||
{t('webview.selectProjectDirectory')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@ export const SessionHeader: React.FC<SessionHeaderProps> = React.memo(({
|
||||
</Badge>
|
||||
{totalTokens > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{totalTokens.toLocaleString()} tokens
|
||||
{totalTokens.toLocaleString()} {t('usage.tokens')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -165,13 +165,13 @@ export const SessionHeader: React.FC<SessionHeaderProps> = React.memo(({
|
||||
{onProjectSettings && projectPath && (
|
||||
<DropdownMenuItem onClick={onProjectSettings}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Project Settings
|
||||
{t('agents.projectSettings')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onSlashCommandsSettings && projectPath && (
|
||||
<DropdownMenuItem onClick={onSlashCommandsSettings}>
|
||||
<Command className="h-4 w-4 mr-2" />
|
||||
Slash Commands
|
||||
{t('slashCommands.slashCommands')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { Terminal, ChevronRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface BashWidgetProps {
|
||||
command: string;
|
||||
@@ -9,6 +10,7 @@ interface BashWidgetProps {
|
||||
}
|
||||
|
||||
export const BashWidget: React.FC<BashWidgetProps> = ({ command, description, result }) => {
|
||||
const { t } = useTranslation();
|
||||
// Extract result content if available
|
||||
let resultContent = '';
|
||||
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="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" />
|
||||
<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 && (
|
||||
<>
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||
@@ -45,7 +47,7 @@ export const BashWidget: React.FC<BashWidgetProps> = ({ command, description, re
|
||||
{!result && (
|
||||
<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" />
|
||||
<span>Running...</span>
|
||||
<span>{t('widgets.common.running')}</span>
|
||||
</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-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>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"name": "Claudia",
|
||||
"welcome": "Welcome to Claudia",
|
||||
"tagline": "Powerful Claude Code session management tool",
|
||||
"returnHome": "Return to Home",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
@@ -99,8 +100,22 @@
|
||||
"justNow": "just now",
|
||||
"minutesAgo": "{{count}} minute{{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": {
|
||||
"projects": "CC Projects",
|
||||
"agents": "Agent Management",
|
||||
@@ -138,7 +153,27 @@
|
||||
"sessions": "Sessions",
|
||||
"noSessions": "No sessions found",
|
||||
"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": {
|
||||
"title": "Agent Management",
|
||||
@@ -153,6 +188,11 @@
|
||||
"enterTask": "Enter the task for the agent",
|
||||
"hooks": "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",
|
||||
"stop": "Stop",
|
||||
"agentName": "Agent Name",
|
||||
@@ -317,7 +357,13 @@
|
||||
"condition": "Condition",
|
||||
"matchesRegex": "Matches regex",
|
||||
"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": {
|
||||
"title": "Settings",
|
||||
@@ -380,6 +426,8 @@
|
||||
"verboseOutputDesc": "Show full bash and command outputs",
|
||||
"chatRetention": "Chat Transcript Retention (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",
|
||||
"choosePreferredInstallation": "Choose your preferred Claude Code installation.",
|
||||
"loadingAvailableInstallations": "Loading available installations...",
|
||||
@@ -761,6 +809,18 @@
|
||||
"checkpointSettingsTitle": "Checkpoint Settings",
|
||||
"experimentalFeature": "Experimental Feature",
|
||||
"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",
|
||||
"automaticCheckpointsDesc": "Automatically create checkpoints based on the selected strategy",
|
||||
"checkpointStrategy": "Checkpoint Strategy",
|
||||
@@ -967,4 +1027,60 @@
|
||||
"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)."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"name": "Claudia",
|
||||
"welcome": "欢迎使用 Claudia",
|
||||
"tagline": "强大的 Claude Code 会话管理工具",
|
||||
"returnHome": "返回首页",
|
||||
"loading": "加载中...",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
@@ -96,7 +97,19 @@
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "{{count}} 分钟前",
|
||||
"hoursAgo": "{{count}} 小时前",
|
||||
"daysAgo": "{{count}} 天前"
|
||||
"daysAgo": "{{count}} 天前",
|
||||
"loadingOutput": "加载输出中...",
|
||||
"waitingForOutput": "等待输出...",
|
||||
"noOutput": "暂无输出",
|
||||
"agentRunningNoOutput": "智能体正在运行,但尚未收到输出",
|
||||
"failedToLoadSessionOutput": "加载会话输出失败",
|
||||
"outputRefreshed": "输出已刷新",
|
||||
"failedToRefreshOutput": "刷新输出失败",
|
||||
"agentExecutionCompleted": "智能体执行已完成",
|
||||
"agentExecutionCancelled": "智能体执行已取消",
|
||||
"loadingAgentRun": "正在加载运行详情...",
|
||||
"selectProjectFirst": "请先选择项目目录",
|
||||
"selectDirectoryFailed": "选择目录失败:{{message}}"
|
||||
},
|
||||
"navigation": {
|
||||
"projects": "Claude Code 项目",
|
||||
@@ -135,7 +148,27 @@
|
||||
"sessions": "会话",
|
||||
"noSessions": "未找到会话",
|
||||
"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": {
|
||||
"title": "Agent 管理",
|
||||
@@ -202,6 +235,11 @@
|
||||
"enterTask": "输入智能体的任务",
|
||||
"hooks": "钩子",
|
||||
"configureHooks": "配置钩子",
|
||||
"configureHooksDesc": "配置在工具执行前、中、后运行的钩子。更改将立即保存。",
|
||||
"projectSettings": "项目设置",
|
||||
"localSettings": "本地设置",
|
||||
"initializing": "正在初始化智能体...",
|
||||
"exportToFile": "导出为 .claudia.json",
|
||||
"fullscreen": "全屏",
|
||||
"stop": "停止",
|
||||
"selectProjectPathAndTask": "选择项目路径并输入任务以运行智能体",
|
||||
@@ -306,7 +344,13 @@
|
||||
"condition": "条件",
|
||||
"matchesRegex": "匹配正则表达式",
|
||||
"message": "消息",
|
||||
"enterShellCommand": "输入 shell 命令..."
|
||||
"enterShellCommand": "输入 shell 命令...",
|
||||
"projectHooks": "项目钩子",
|
||||
"localHooks": "本地钩子"
|
||||
},
|
||||
"projectSettings": {
|
||||
"gitignoreLocalWarning": "本地设置文件未添加到 .gitignore",
|
||||
"addToGitignore": "添加到 .gitignore"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
@@ -369,6 +413,8 @@
|
||||
"verboseOutputDesc": "显示完整的 bash 和命令输出",
|
||||
"chatRetention": "聊天记录保留期 (天)",
|
||||
"chatRetentionDesc": "本地保留聊天记录的时长(默认:30 天)",
|
||||
"chatRetentionPermanent": "已启用永久保存,聊天记录将不会被自动删除",
|
||||
"permanentRetention": "永久保存",
|
||||
"claudeCodeInstallation": "Claude Code 安装",
|
||||
"choosePreferredInstallation": "选择您偏好的 Claude Code 安装。",
|
||||
"loadingAvailableInstallations": "正在加载可用安装...",
|
||||
@@ -678,6 +724,9 @@
|
||||
"checkpoint": {
|
||||
"title": "检查点",
|
||||
"createCheckpoint": "创建检查点",
|
||||
"createCheckpointDesc": "保存会话当前状态,并可填写可选说明。",
|
||||
"descriptionOptional": "描述(可选)",
|
||||
"descriptionPlaceholder": "例如:大型重构前",
|
||||
"restoreCheckpoint": "恢复检查点",
|
||||
"deleteCheckpoint": "删除检查点",
|
||||
"checkpointName": "检查点名称",
|
||||
@@ -688,6 +737,15 @@
|
||||
"checkpointSettingsTitle": "检查点设置",
|
||||
"experimentalFeature": "实验性功能",
|
||||
"checkpointWarning": "检查点可能会影响目录结构或导致数据丢失。请谨慎使用。",
|
||||
"current": "当前",
|
||||
"restoreToThis": "恢复到此检查点",
|
||||
"forkFromThis": "从此检查点分叉",
|
||||
"compareWithAnother": "与另一个检查点比较",
|
||||
"checkpointComparison": "检查点对比",
|
||||
"modifiedFiles": "修改的文件",
|
||||
"addedFiles": "新增的文件",
|
||||
"deletedFiles": "删除的文件",
|
||||
"noPrompt": "无提示",
|
||||
"automaticCheckpoints": "自动检查点",
|
||||
"automaticCheckpointsDesc": "根据所选策略自动创建检查点",
|
||||
"checkpointStrategy": "检查点策略",
|
||||
@@ -894,4 +952,60 @@
|
||||
"warning": {
|
||||
"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 为空表)。"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user