diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 8a6314c..7e23349 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -1,12 +1,28 @@ import { type ReactNode } from 'react'; import { useLayout } from '../../contexts/LayoutContext'; import { useNetwork } from '../../hooks'; -import { useUpdateCheck } from '../../hooks/useUpdateCheck'; +import { useUpdateCheck, type UpdatePhase } from '../../hooks/useUpdateCheck'; import { OfflineIndicator } from '../shared'; import { TabSwitcher, ConversationSidebar } from '../conversations'; import { EmployeePanel } from '../employees'; import { TrialBanner } from '../trial/TrialBanner'; +function updateButtonLabel(phase: UpdatePhase): string { + switch (phase) { + case 'downloading': return 'Downloading…'; + case 'relaunching': return 'Relaunching…'; + default: return 'Update Available'; + } +} + +function friendlyUpdateError(raw: string): string { + const lower = raw.toLowerCase(); + if (lower.includes('sandbox')) return 'Update blocked by sandbox'; + if (lower.includes('signature') || lower.includes('sig ')) return 'Update signature invalid'; + if (lower.includes('network') || lower.includes('fetch') || lower.includes('request')) return 'Network error during update'; + return 'Update failed'; +} + interface AppShellProps { children: ReactNode; contextPanel?: ReactNode; @@ -79,7 +95,7 @@ function IconButton({ export function AppShell({ children, contextPanel, onSettingsClick }: AppShellProps) { const { sidebarOpen, contextPanelOpen, sidebarTab, toggleSidebar, toggleContextPanel, setSidebarTab } = useLayout(); const { isOnline, errorMessage, checkNow, isChecking } = useNetwork(); - const { updateAvailable, installing, installUpdate } = useUpdateCheck(); + const { updateAvailable, phase, installing, error: updateError, installUpdate, retry: retryUpdate } = useUpdateCheck(); return (
@@ -115,7 +131,45 @@ export function AppShell({ children, contextPanel, onSettingsClick }: AppShellPr />
- {updateAvailable && ( + {updateError ? ( +
+ + {friendlyUpdateError(updateError)} + +
+ ) : updateAvailable && ( )} diff --git a/src/hooks/useUpdateCheck.ts b/src/hooks/useUpdateCheck.ts index 1d1b4a4..52aec90 100644 --- a/src/hooks/useUpdateCheck.ts +++ b/src/hooks/useUpdateCheck.ts @@ -2,18 +2,20 @@ import { useEffect, useState } from 'react'; import { check, Update } from '@tauri-apps/plugin-updater'; import { relaunch } from '@tauri-apps/plugin-process'; +export type UpdatePhase = 'idle' | 'checking' | 'downloading' | 'relaunching'; + export function useUpdateCheck() { const [updateAvailable, setUpdateAvailable] = useState(null); - const [checking, setChecking] = useState(false); - const [installing, setInstalling] = useState(false); + const [phase, setPhase] = useState('idle'); const [error, setError] = useState(null); useEffect(() => { - checkForUpdate(); + void checkForUpdate(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function checkForUpdate() { - setChecking(true); + setPhase('checking'); setError(null); try { const update = await check(); @@ -23,21 +25,47 @@ export function useUpdateCheck() { } catch (e) { setError(e instanceof Error ? e.message : 'Failed to check for updates'); } finally { - setChecking(false); + setPhase('idle'); } } async function installUpdate() { if (!updateAvailable) return; - setInstalling(true); + setError(null); + setPhase('downloading'); try { - await updateAvailable.downloadAndInstall(); + await updateAvailable.downloadAndInstall((progress) => { + if (progress.event === 'Finished') { + setPhase('relaunching'); + } + }); await relaunch(); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to install update'); - setInstalling(false); + setPhase('idle'); } } - return { updateAvailable, checking, installing, error, checkForUpdate, installUpdate }; + async function retry() { + setError(null); + if (updateAvailable) { + await installUpdate(); + } else { + await checkForUpdate(); + } + } + + const checking = phase === 'checking'; + const installing = phase === 'downloading' || phase === 'relaunching'; + + return { + updateAvailable, + phase, + checking, + installing, + error, + checkForUpdate, + installUpdate, + retry, + }; }