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,
+ };
}