feat: add Claude Code changelog link to version notifiers (#820)

* feat: add Claude Code changelog link to version notifiers

Add link to Claude Code Changelog in both:
- Claude Code CLI status badge popover
- App Update Notification dialog

The link opens https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
in external browser, allowing users to check what's new in Claude Code.

Also converts AppUpdateNotification to use i18n translations.

Fixes #817

* refactor: improve AppUpdateNotification code quality

- Extract CLAUDE_CODE_CHANGELOG_URL to named constant
- Remove unused "common" namespace from useTranslation hook

---------

Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
This commit is contained in:
StillKnotKnown
2026-01-08 16:01:16 +02:00
committed by GitHub
parent cbb1cb8154
commit ada91fb195
6 changed files with 230 additions and 141 deletions
@@ -1,24 +1,20 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from "react";
import { import { useTranslation } from "react-i18next";
Download, import { Download, RefreshCw, CheckCircle2, AlertCircle, ExternalLink } from "lucide-react";
RefreshCw, import { Button } from "./ui/button";
CheckCircle2, import { Progress } from "./ui/progress";
AlertCircle
} from 'lucide-react';
import { Button } from './ui/button';
import { Progress } from './ui/progress';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle DialogTitle,
} from './ui/dialog'; } from "./ui/dialog";
import type { import type { AppUpdateAvailableEvent, AppUpdateProgress } from "../../shared/types";
AppUpdateAvailableEvent,
AppUpdateProgress const CLAUDE_CODE_CHANGELOG_URL =
} from '../../shared/types'; "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md";
/** /**
* Simple markdown renderer for release notes * Simple markdown renderer for release notes
@@ -28,23 +24,32 @@ function ReleaseNotesRenderer({ markdown }: { markdown: string }) {
const html = useMemo(() => { const html = useMemo(() => {
const result = markdown const result = markdown
// Escape HTML // Escape HTML
.replace(/&/g, '&amp;') .replace(/&/g, "&amp;")
.replace(/</g, '&lt;') .replace(/</g, "&lt;")
.replace(/>/g, '&gt;') .replace(/>/g, "&gt;")
// Headers (### Header -> <h3>) // Headers (### Header -> <h3>)
.replace(/^### (.+)$/gm, '<h4 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h4>') .replace(
.replace(/^## (.+)$/gm, '<h3 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h3>') /^### (.+)$/gm,
'<h4 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h4>'
)
.replace(
/^## (.+)$/gm,
'<h3 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h3>'
)
// Bold (**text** -> <strong>) // Bold (**text** -> <strong>)
.replace(/\*\*([^*]+)\*\*/g, '<strong class="text-foreground font-medium">$1</strong>') .replace(/\*\*([^*]+)\*\*/g, '<strong class="text-foreground font-medium">$1</strong>')
// Inline code (`code` -> <code>) // Inline code (`code` -> <code>)
.replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 bg-muted rounded text-xs">$1</code>') .replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 bg-muted rounded text-xs">$1</code>')
// List items (- item -> <li>) // List items (- item -> <li>)
.replace(/^- (.+)$/gm, '<li class="ml-4 text-muted-foreground before:content-[\'•\'] before:mr-2 before:text-muted-foreground/60">$1</li>') .replace(
/^- (.+)$/gm,
"<li class=\"ml-4 text-muted-foreground before:content-['•'] before:mr-2 before:text-muted-foreground/60\">$1</li>"
)
// Wrap consecutive list items // Wrap consecutive list items
.replace(/(<li[^>]*>.*?<\/li>\n?)+/g, '<ul class="space-y-1 my-2">$&</ul>') .replace(/(<li[^>]*>.*?<\/li>\n?)+/g, '<ul class="space-y-1 my-2">$&</ul>')
// Line breaks for remaining lines // Line breaks for remaining lines
.replace(/\n\n/g, '<div class="h-2"></div>') .replace(/\n\n/g, '<div class="h-2"></div>')
.replace(/\n/g, '<br/>'); .replace(/\n/g, "<br/>");
return result; return result;
}, [markdown]); }, [markdown]);
@@ -62,6 +67,7 @@ function ReleaseNotesRenderer({ markdown }: { markdown: string }) {
* Shows when a new app version is available and handles download/install workflow * Shows when a new app version is available and handles download/install workflow
*/ */
export function AppUpdateNotification() { export function AppUpdateNotification() {
const { t } = useTranslation(["dialogs"]);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [updateInfo, setUpdateInfo] = useState<AppUpdateAvailableEvent | null>(null); const [updateInfo, setUpdateInfo] = useState<AppUpdateAvailableEvent | null>(null);
const [downloadProgress, setDownloadProgress] = useState<AppUpdateProgress | null>(null); const [downloadProgress, setDownloadProgress] = useState<AppUpdateProgress | null>(null);
@@ -109,12 +115,14 @@ export function AppUpdateNotification() {
try { try {
const result = await window.electronAPI.downloadAppUpdate(); const result = await window.electronAPI.downloadAppUpdate();
if (!result.success) { if (!result.success) {
setDownloadError(result.error || 'Failed to download update'); setDownloadError(
result.error || t("dialogs:appUpdate.downloadError", "Failed to download update")
);
setIsDownloading(false); setIsDownloading(false);
} }
} catch (error) { } catch (error) {
console.error('Failed to download app update:', error); console.error("Failed to download app update:", error);
setDownloadError('Failed to download update'); setDownloadError(t("dialogs:appUpdate.downloadError", "Failed to download update"));
setIsDownloading(false); setIsDownloading(false);
} }
}; };
@@ -137,10 +145,13 @@ export function AppUpdateNotification() {
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Download className="h-5 w-5" /> <Download className="h-5 w-5" />
App Update Available {t("dialogs:appUpdate.title", "App Update Available")}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
A new version of Auto Claude is ready to download {t(
"dialogs:appUpdate.description",
"A new version of Auto Claude is ready to download"
)}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -150,14 +161,13 @@ export function AppUpdateNotification() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-1"> <p className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
New Version {t("dialogs:appUpdate.newVersion", "New Version")}
</p>
<p className="text-base font-medium text-foreground">
{updateInfo.version}
</p> </p>
<p className="text-base font-medium text-foreground">{updateInfo.version}</p>
{updateInfo.releaseDate && ( {updateInfo.releaseDate && (
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
Released {new Date(updateInfo.releaseDate).toLocaleDateString()} {t("dialogs:appUpdate.released", "Released")}{" "}
{new Date(updateInfo.releaseDate).toLocaleDateString()}
</p> </p>
)} )}
</div> </div>
@@ -178,18 +188,36 @@ export function AppUpdateNotification() {
</div> </div>
)} )}
{/* Claude Code Changelog Link */}
<Button
variant="link"
size="sm"
className="w-full text-xs text-muted-foreground gap-1"
onClick={() => window.electronAPI?.openExternal?.(CLAUDE_CODE_CHANGELOG_URL)}
aria-label={t(
"dialogs:appUpdate.claudeCodeChangelogAriaLabel",
"View Claude Code Changelog (opens in new window)"
)}
>
{t("dialogs:appUpdate.claudeCodeChangelog", "View Claude Code Changelog")}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</Button>
{/* Download Progress */} {/* Download Progress */}
{isDownloading && downloadProgress && ( {isDownloading && downloadProgress && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Downloading...</span> <span className="text-muted-foreground">
{t("dialogs:appUpdate.downloading", "Downloading...")}
</span>
<span className="text-foreground font-medium"> <span className="text-foreground font-medium">
{Math.round(downloadProgress.percent)}% {Math.round(downloadProgress.percent)}%
</span> </span>
</div> </div>
<Progress value={downloadProgress.percent} className="h-2" /> <Progress value={downloadProgress.percent} className="h-2" />
<p className="text-xs text-muted-foreground text-right"> <p className="text-xs text-muted-foreground text-right">
{(downloadProgress.transferred / 1024 / 1024).toFixed(2)} MB / {(downloadProgress.total / 1024 / 1024).toFixed(2)} MB {(downloadProgress.transferred / 1024 / 1024).toFixed(2)} MB /{" "}
{(downloadProgress.total / 1024 / 1024).toFixed(2)} MB
</p> </p>
</div> </div>
)} )}
@@ -206,39 +234,39 @@ export function AppUpdateNotification() {
{isDownloaded && ( {isDownloaded && (
<div className="flex items-center gap-3 text-sm text-success bg-success/10 border border-success/30 rounded-lg p-3"> <div className="flex items-center gap-3 text-sm text-success bg-success/10 border border-success/30 rounded-lg p-3">
<CheckCircle2 className="h-5 w-5 shrink-0" /> <CheckCircle2 className="h-5 w-5 shrink-0" />
<span>Update downloaded successfully! Click Install to restart and apply the update.</span> <span>
{t(
"dialogs:appUpdate.updateDownloaded",
"Update downloaded successfully! Click Install to restart and apply the update."
)}
</span>
</div> </div>
)} )}
</div> </div>
<DialogFooter className="flex flex-col sm:flex-row gap-3"> <DialogFooter className="flex flex-col sm:flex-row gap-3">
<Button <Button variant="outline" onClick={handleDismiss} disabled={isDownloading}>
variant="outline" {isDownloaded
onClick={handleDismiss} ? t("dialogs:appUpdate.installLater", "Install Later")
disabled={isDownloading} : t("dialogs:appUpdate.remindMeLater", "Remind Me Later")}
>
{isDownloaded ? 'Install Later' : 'Remind Me Later'}
</Button> </Button>
{isDownloaded ? ( {isDownloaded ? (
<Button onClick={handleInstall}> <Button onClick={handleInstall}>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
Install and Restart {t("dialogs:appUpdate.installAndRestart", "Install and Restart")}
</Button> </Button>
) : ( ) : (
<Button <Button onClick={handleDownload} disabled={isDownloading}>
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? ( {isDownloading ? (
<> <>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" /> <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Downloading... {t("dialogs:appUpdate.downloading", "Downloading...")}
</> </>
) : ( ) : (
<> <>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Download Update {t("dialogs:appUpdate.downloadUpdate", "Download Update")}
</> </>
)} )}
</Button> </Button>
@@ -1,17 +1,18 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { Terminal, Check, AlertTriangle, X, Loader2, Download, RefreshCw, ExternalLink } from 'lucide-react';
import { Button } from './ui/button';
import { import {
Popover, Terminal,
PopoverContent, Check,
PopoverTrigger AlertTriangle,
} from './ui/popover'; X,
import { Loader2,
Tooltip, Download,
TooltipContent, RefreshCw,
TooltipTrigger ExternalLink,
} from './ui/tooltip'; } from "lucide-react";
import { Button } from "./ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -20,16 +21,16 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle AlertDialogTitle,
} from './ui/alert-dialog'; } from "./ui/alert-dialog";
import { cn } from '../lib/utils'; import { cn } from "../lib/utils";
import type { ClaudeCodeVersionInfo } from '../../shared/types/cli'; import type { ClaudeCodeVersionInfo } from "../../shared/types/cli";
interface ClaudeCodeStatusBadgeProps { interface ClaudeCodeStatusBadgeProps {
className?: string; className?: string;
} }
type StatusType = 'loading' | 'installed' | 'outdated' | 'not-found' | 'error'; type StatusType = "loading" | "installed" | "outdated" | "not-found" | "error";
// Check every 24 hours // Check every 24 hours
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -39,8 +40,8 @@ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
* Shows installation status and provides quick access to install/update. * Shows installation status and provides quick access to install/update.
*/ */
export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps) { export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps) {
const { t } = useTranslation(['common', 'navigation']); const { t } = useTranslation(["common", "navigation"]);
const [status, setStatus] = useState<StatusType>('loading'); const [status, setStatus] = useState<StatusType>("loading");
const [versionInfo, setVersionInfo] = useState<ClaudeCodeVersionInfo | null>(null); const [versionInfo, setVersionInfo] = useState<ClaudeCodeVersionInfo | null>(null);
const [isInstalling, setIsInstalling] = useState(false); const [isInstalling, setIsInstalling] = useState(false);
const [lastChecked, setLastChecked] = useState<Date | null>(null); const [lastChecked, setLastChecked] = useState<Date | null>(null);
@@ -51,7 +52,7 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
const checkVersion = useCallback(async () => { const checkVersion = useCallback(async () => {
try { try {
if (!window.electronAPI?.checkClaudeCodeVersion) { if (!window.electronAPI?.checkClaudeCodeVersion) {
setStatus('error'); setStatus("error");
return; return;
} }
@@ -62,18 +63,18 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
setLastChecked(new Date()); setLastChecked(new Date());
if (!result.data.installed) { if (!result.data.installed) {
setStatus('not-found'); setStatus("not-found");
} else if (result.data.isOutdated) { } else if (result.data.isOutdated) {
setStatus('outdated'); setStatus("outdated");
} else { } else {
setStatus('installed'); setStatus("installed");
} }
} else { } else {
setStatus('error'); setStatus("error");
} }
} catch (err) { } catch (err) {
console.error('Failed to check Claude Code version:', err); console.error("Failed to check Claude Code version:", err);
setStatus('error'); setStatus("error");
} }
}, []); }, []);
@@ -107,7 +108,7 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
}, 5000); }, 5000);
} }
} catch (err) { } catch (err) {
console.error('Failed to install Claude Code:', err); console.error("Failed to install Claude Code:", err);
} finally { } finally {
setIsInstalling(false); setIsInstalling(false);
} }
@@ -115,7 +116,7 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
// Handle install/update button click // Handle install/update button click
const handleInstall = () => { const handleInstall = () => {
if (status === 'outdated') { if (status === "outdated") {
// Show warning for updates since it will close running Claude sessions // Show warning for updates since it will close running Claude sessions
setShowUpdateWarning(true); setShowUpdateWarning(true);
} else { } else {
@@ -127,30 +128,30 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
// Get status indicator color // Get status indicator color
const getStatusColor = () => { const getStatusColor = () => {
switch (status) { switch (status) {
case 'installed': case "installed":
return 'bg-green-500'; return "bg-green-500";
case 'outdated': case "outdated":
return 'bg-yellow-500'; return "bg-yellow-500";
case 'not-found': case "not-found":
case 'error': case "error":
return 'bg-destructive'; return "bg-destructive";
default: default:
return 'bg-muted-foreground'; return "bg-muted-foreground";
} }
}; };
// Get status icon // Get status icon
const getStatusIcon = () => { const getStatusIcon = () => {
switch (status) { switch (status) {
case 'loading': case "loading":
return <Loader2 className="h-3 w-3 animate-spin" />; return <Loader2 className="h-3 w-3 animate-spin" />;
case 'installed': case "installed":
return <Check className="h-3 w-3" />; return <Check className="h-3 w-3" />;
case 'outdated': case "outdated":
return <AlertTriangle className="h-3 w-3" />; return <AlertTriangle className="h-3 w-3" />;
case 'not-found': case "not-found":
return <X className="h-3 w-3" />; return <X className="h-3 w-3" />;
case 'error': case "error":
return <AlertTriangle className="h-3 w-3" />; return <AlertTriangle className="h-3 w-3" />;
} }
}; };
@@ -158,16 +159,16 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
// Get tooltip text // Get tooltip text
const getTooltipText = () => { const getTooltipText = () => {
switch (status) { switch (status) {
case 'loading': case "loading":
return t('navigation:claudeCode.checking', 'Checking Claude Code...'); return t("navigation:claudeCode.checking", "Checking Claude Code...");
case 'installed': case "installed":
return t('navigation:claudeCode.upToDate', 'Claude Code is up to date'); return t("navigation:claudeCode.upToDate", "Claude Code is up to date");
case 'outdated': case "outdated":
return t('navigation:claudeCode.updateAvailable', 'Claude Code update available'); return t("navigation:claudeCode.updateAvailable", "Claude Code update available");
case 'not-found': case "not-found":
return t('navigation:claudeCode.notInstalled', 'Claude Code not installed'); return t("navigation:claudeCode.notInstalled", "Claude Code not installed");
case 'error': case "error":
return t('navigation:claudeCode.error', 'Error checking Claude Code'); return t("navigation:claudeCode.error", "Error checking Claude Code");
} }
}; };
@@ -180,36 +181,36 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
variant="ghost" variant="ghost"
size="sm" size="sm"
className={cn( className={cn(
'w-full justify-start gap-2 text-xs', "w-full justify-start gap-2 text-xs",
status === 'not-found' || status === 'error' ? 'text-destructive' : '', status === "not-found" || status === "error" ? "text-destructive" : "",
status === 'outdated' ? 'text-yellow-600 dark:text-yellow-500' : '', status === "outdated" ? "text-yellow-600 dark:text-yellow-500" : "",
className className
)} )}
> >
<div className="relative"> <div className="relative">
<Terminal className="h-4 w-4" /> <Terminal className="h-4 w-4" />
<span className={cn( <span
'absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full', className={cn(
getStatusColor() "absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full",
)} /> getStatusColor()
)}
/>
</div> </div>
<span className="truncate">Claude Code</span> <span className="truncate">Claude Code</span>
{status === 'outdated' && ( {status === "outdated" && (
<span className="ml-auto text-[10px] bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 px-1.5 py-0.5 rounded"> <span className="ml-auto text-[10px] bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 px-1.5 py-0.5 rounded">
{t('common:update', 'Update')} {t("common:update", "Update")}
</span> </span>
)} )}
{status === 'not-found' && ( {status === "not-found" && (
<span className="ml-auto text-[10px] bg-destructive/20 text-destructive px-1.5 py-0.5 rounded"> <span className="ml-auto text-[10px] bg-destructive/20 text-destructive px-1.5 py-0.5 rounded">
{t('common:install', 'Install')} {t("common:install", "Install")}
</span> </span>
)} )}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right"> <TooltipContent side="right">{getTooltipText()}</TooltipContent>
{getTooltipText()}
</TooltipContent>
</Tooltip> </Tooltip>
<PopoverContent side="right" align="end" className="w-72"> <PopoverContent side="right" align="end" className="w-72">
@@ -223,33 +224,37 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
<h4 className="text-sm font-medium">Claude Code CLI</h4> <h4 className="text-sm font-medium">Claude Code CLI</h4>
<p className="text-xs text-muted-foreground flex items-center gap-1"> <p className="text-xs text-muted-foreground flex items-center gap-1">
{getStatusIcon()} {getStatusIcon()}
{status === 'installed' && t('navigation:claudeCode.installed', 'Installed')} {status === "installed" && t("navigation:claudeCode.installed", "Installed")}
{status === 'outdated' && t('navigation:claudeCode.outdated', 'Update available')} {status === "outdated" && t("navigation:claudeCode.outdated", "Update available")}
{status === 'not-found' && t('navigation:claudeCode.missing', 'Not installed')} {status === "not-found" && t("navigation:claudeCode.missing", "Not installed")}
{status === 'loading' && t('navigation:claudeCode.checking', 'Checking...')} {status === "loading" && t("navigation:claudeCode.checking", "Checking...")}
{status === 'error' && t('navigation:claudeCode.error', 'Error')} {status === "error" && t("navigation:claudeCode.error", "Error")}
</p> </p>
</div> </div>
</div> </div>
{/* Version info */} {/* Version info */}
{versionInfo && status !== 'loading' && ( {versionInfo && status !== "loading" && (
<div className="text-xs space-y-1 p-2 bg-muted rounded-md"> <div className="text-xs space-y-1 p-2 bg-muted rounded-md">
{versionInfo.installed && ( {versionInfo.installed && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground">{t('navigation:claudeCode.current', 'Current')}:</span> <span className="text-muted-foreground">
{t("navigation:claudeCode.current", "Current")}:
</span>
<span className="font-mono">{versionInfo.installed}</span> <span className="font-mono">{versionInfo.installed}</span>
</div> </div>
)} )}
{versionInfo.latest && versionInfo.latest !== 'unknown' && ( {versionInfo.latest && versionInfo.latest !== "unknown" && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground">{t('navigation:claudeCode.latest', 'Latest')}:</span> <span className="text-muted-foreground">
{t("navigation:claudeCode.latest", "Latest")}:
</span>
<span className="font-mono">{versionInfo.latest}</span> <span className="font-mono">{versionInfo.latest}</span>
</div> </div>
)} )}
{lastChecked && ( {lastChecked && (
<div className="flex justify-between text-muted-foreground"> <div className="flex justify-between text-muted-foreground">
<span>{t('navigation:claudeCode.lastChecked', 'Last checked')}:</span> <span>{t("navigation:claudeCode.lastChecked", "Last checked")}:</span>
<span>{lastChecked.toLocaleTimeString()}</span> <span>{lastChecked.toLocaleTimeString()}</span>
</div> </div>
)} )}
@@ -258,7 +263,7 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
{/* Actions */} {/* Actions */}
<div className="flex gap-2"> <div className="flex gap-2">
{(status === 'not-found' || status === 'outdated') && ( {(status === "not-found" || status === "outdated") && (
<Button <Button
size="sm" size="sm"
className="flex-1 gap-1" className="flex-1 gap-1"
@@ -270,10 +275,9 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
) : ( ) : (
<Download className="h-3 w-3" /> <Download className="h-3 w-3" />
)} )}
{status === 'outdated' {status === "outdated"
? t('common:update', 'Update') ? t("common:update", "Update")
: t('common:install', 'Install') : t("common:install", "Install")}
}
</Button> </Button>
)} )}
<Button <Button
@@ -281,10 +285,10 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
size="sm" size="sm"
className="gap-1" className="gap-1"
onClick={() => checkVersion()} onClick={() => checkVersion()}
disabled={status === 'loading'} disabled={status === "loading"}
> >
<RefreshCw className={cn('h-3 w-3', status === 'loading' && 'animate-spin')} /> <RefreshCw className={cn("h-3 w-3", status === "loading" && "animate-spin")} />
{t('common:refresh', 'Refresh')} {t("common:refresh", "Refresh")}
</Button> </Button>
</div> </div>
@@ -293,10 +297,32 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
variant="link" variant="link"
size="sm" size="sm"
className="w-full text-xs text-muted-foreground gap-1" className="w-full text-xs text-muted-foreground gap-1"
onClick={() => window.electronAPI?.openExternal?.('https://claude.ai/code')} onClick={() => window.electronAPI?.openExternal?.("https://claude.ai/code")}
aria-label={t('navigation:claudeCode.learnMoreAriaLabel', 'Learn more about Claude Code (opens in new window)')} aria-label={t(
"navigation:claudeCode.learnMoreAriaLabel",
"Learn more about Claude Code (opens in new window)"
)}
> >
{t('navigation:claudeCode.learnMore', 'Learn more about Claude Code')} {t("navigation:claudeCode.learnMore", "Learn more about Claude Code")}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</Button>
{/* Changelog link */}
<Button
variant="link"
size="sm"
className="w-full text-xs text-muted-foreground gap-1"
onClick={() =>
window.electronAPI?.openExternal?.(
"https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"
)
}
aria-label={t(
"navigation:claudeCode.viewChangelogAriaLabel",
"View Claude Code Changelog (opens in new window)"
)}
>
{t("navigation:claudeCode.viewChangelog", "View Claude Code Changelog")}
<ExternalLink className="h-3 w-3" aria-hidden="true" /> <ExternalLink className="h-3 w-3" aria-hidden="true" />
</Button> </Button>
</div> </div>
@@ -307,18 +333,19 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
{t('navigation:claudeCode.updateWarningTitle', 'Update Claude Code?')} {t("navigation:claudeCode.updateWarningTitle", "Update Claude Code?")}
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t('navigation:claudeCode.updateWarningDescription', 'Updating will close all running Claude Code sessions. Any unsaved work in those sessions may be lost. Make sure to save your work before proceeding.')} {t(
"navigation:claudeCode.updateWarningDescription",
"Updating will close all running Claude Code sessions. Any unsaved work in those sessions may be lost. Make sure to save your work before proceeding."
)}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel> <AlertDialogCancel>{t("common:cancel", "Cancel")}</AlertDialogCancel>
{t('common:cancel', 'Cancel')}
</AlertDialogCancel>
<AlertDialogAction onClick={performInstall}> <AlertDialogAction onClick={performInstall}>
{t('navigation:claudeCode.updateAnyway', 'Update Anyway')} {t("navigation:claudeCode.updateAnyway", "Update Anyway")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -133,5 +133,20 @@
"cancel": "Cancel", "cancel": "Cancel",
"remove": "Remove", "remove": "Remove",
"error": "Failed to remove project" "error": "Failed to remove project"
},
"appUpdate": {
"title": "App Update Available",
"description": "A new version of Auto Claude is ready to download",
"newVersion": "New Version",
"released": "Released",
"downloading": "Downloading...",
"downloadUpdate": "Download Update",
"installAndRestart": "Install and Restart",
"installLater": "Install Later",
"remindMeLater": "Remind Me Later",
"updateDownloaded": "Update downloaded successfully! Click Install to restart and apply the update.",
"downloadError": "Failed to download update",
"claudeCodeChangelog": "View Claude Code Changelog",
"claudeCodeChangelogAriaLabel": "View Claude Code Changelog (opens in new window)"
} }
} }
@@ -44,6 +44,8 @@
"lastChecked": "Last checked", "lastChecked": "Last checked",
"learnMore": "Learn more about Claude Code", "learnMore": "Learn more about Claude Code",
"learnMoreAriaLabel": "Learn more about Claude Code (opens in new window)", "learnMoreAriaLabel": "Learn more about Claude Code (opens in new window)",
"viewChangelog": "View Claude Code Changelog",
"viewChangelogAriaLabel": "View Claude Code Changelog (opens in new window)",
"updateWarningTitle": "Update Claude Code?", "updateWarningTitle": "Update Claude Code?",
"updateWarningDescription": "Updating will close all running Claude Code sessions. Any unsaved work in those sessions may be lost. Make sure to save your work before proceeding.", "updateWarningDescription": "Updating will close all running Claude Code sessions. Any unsaved work in those sessions may be lost. Make sure to save your work before proceeding.",
"updateAnyway": "Update Anyway" "updateAnyway": "Update Anyway"
@@ -133,5 +133,20 @@
"cancel": "Annuler", "cancel": "Annuler",
"remove": "Retirer", "remove": "Retirer",
"error": "Échec de la suppression du projet" "error": "Échec de la suppression du projet"
},
"appUpdate": {
"title": "Mise à jour de l'application disponible",
"description": "Une nouvelle version d'Auto Claude est prête à être téléchargée",
"newVersion": "Nouvelle version",
"released": "Publiée",
"downloading": "Téléchargement...",
"downloadUpdate": "Télécharger la mise à jour",
"installAndRestart": "Installer et redémarrer",
"installLater": "Installer plus tard",
"remindMeLater": "Me rappeler plus tard",
"updateDownloaded": "Mise à jour téléchargée avec succès ! Cliquez sur Installer pour redémarrer et appliquer la mise à jour.",
"downloadError": "Échec du téléchargement de la mise à jour",
"claudeCodeChangelog": "Voir le journal des modifications Claude Code",
"claudeCodeChangelogAriaLabel": "Voir le journal des modifications Claude Code (s'ouvre dans une nouvelle fenêtre)"
} }
} }
@@ -44,6 +44,8 @@
"lastChecked": "Dernière vérification", "lastChecked": "Dernière vérification",
"learnMore": "En savoir plus sur Claude Code", "learnMore": "En savoir plus sur Claude Code",
"learnMoreAriaLabel": "En savoir plus sur Claude Code (s'ouvre dans une nouvelle fenêtre)", "learnMoreAriaLabel": "En savoir plus sur Claude Code (s'ouvre dans une nouvelle fenêtre)",
"viewChangelog": "Voir le journal des modifications Claude Code",
"viewChangelogAriaLabel": "Voir le journal des modifications Claude Code (s'ouvre dans une nouvelle fenêtre)",
"updateWarningTitle": "Mettre à jour Claude Code ?", "updateWarningTitle": "Mettre à jour Claude Code ?",
"updateWarningDescription": "La mise à jour fermera toutes les sessions Claude Code en cours. Tout travail non sauvegardé dans ces sessions pourrait être perdu. Assurez-vous de sauvegarder votre travail avant de continuer.", "updateWarningDescription": "La mise à jour fermera toutes les sessions Claude Code en cours. Tout travail non sauvegardé dans ces sessions pourrait être perdu. Assurez-vous de sauvegarder votre travail avant de continuer.",
"updateAnyway": "Mettre à jour quand même" "updateAnyway": "Mettre à jour quand même"