@@ -12,7 +12,9 @@ import { Download, AlertTriangle, X, RotateCw } from 'lucide-react';
1212import { useTranslation } from 'react-i18next';
1313import { toast } from "sonner";
1414import type { UseUpdaterReturn } from '@/hooks/useUpdater';
15+ import { useModal } from "@/contexts/modal";
1516import { resolveUpdateErrorMessage } from '@/utils/updateError';
17+ import { buildUpdateDiagnostics } from '@/utils/updateDiagnostics';
1618
1719interface SimpleUpdateModalProps {
1820 updater: UseUpdaterReturn;
@@ -22,6 +24,35 @@ interface SimpleUpdateModalProps {
2224 onSkipVersion: () => Promise<void> | void;
2325}
2426
27+ function getTrimmedString(value: unknown): string | null {
28+ if (typeof value !== 'string') return null;
29+ const trimmed = value.trim();
30+ return trimmed.length > 0 ? trimmed : null;
31+ }
32+
33+ function resolveReleaseName(updateInfo: UseUpdaterReturn['state']['updateInfo']): string | null {
34+ if (!updateInfo) return null;
35+
36+ return (
37+ getTrimmedString(updateInfo.rawJson?.releaseName) ??
38+ getTrimmedString(updateInfo.rawJson?.name) ??
39+ getTrimmedString(updateInfo.rawJson?.title) ??
40+ null
41+ );
42+ }
43+
44+ function resolveReleaseNotes(updateInfo: UseUpdaterReturn['state']['updateInfo']): string | null {
45+ if (!updateInfo) return null;
46+
47+ return (
48+ getTrimmedString(updateInfo.body) ??
49+ getTrimmedString(updateInfo.rawJson?.notes) ??
50+ getTrimmedString(updateInfo.rawJson?.releaseNotes) ??
51+ getTrimmedString(updateInfo.rawJson?.changelog) ??
52+ null
53+ );
54+ }
55+
2556export function SimpleUpdateModal({
2657 updater,
2758 isVisible,
@@ -30,11 +61,14 @@ export function SimpleUpdateModal({
3061 onSkipVersion,
3162}: SimpleUpdateModalProps) {
3263 const { t } = useTranslation();
64+ const { openModal } = useModal();
3365
3466 if (!updater.state.hasUpdate) return null;
3567
3668 const currentVersion = updater.state.currentVersion;
3769 const newVersion = updater.state.newVersion || 'unknown';
70+ const releaseName = resolveReleaseName(updater.state.updateInfo);
71+ const releaseNotes = resolveReleaseNotes(updater.state.updateInfo);
3872
3973 const handleDownload = () => {
4074 void updater.downloadAndInstall();
@@ -64,6 +98,32 @@ export function SimpleUpdateModal({
6498 ? resolveUpdateErrorMessage(updater.state.error, t)
6599 : null;
66100
101+ const handleReportIssue = () => {
102+ if (!updater.state.error) return;
103+
104+ const diagnostics = buildUpdateDiagnostics({
105+ error: updater.state.error,
106+ state: updater.state,
107+ });
108+
109+ const subject = t('simpleUpdateModal.reportIssueSubject', {
110+ currentVersion,
111+ newVersion,
112+ });
113+ const body = [t('simpleUpdateModal.reportIssuePrompt'), '', diagnostics].join('\n');
114+
115+ openModal("feedback", {
116+ feedbackPrefill: {
117+ feedbackType: "bug",
118+ subject,
119+ body,
120+ includeSystemInfo: true,
121+ },
122+ });
123+
124+ onClose();
125+ };
126+
67127 return (
68128 <Dialog open={isVisible} onOpenChange={onClose}>
69129 <DialogContent className="max-w-md">
@@ -90,6 +150,30 @@ export function SimpleUpdateModal({
90150 </div>
91151 </div>
92152
153+ {(releaseName || releaseNotes) && (
154+ <div className="space-y-1.5 p-2.5 bg-muted/40 border border-border/60 rounded-md">
155+ {releaseName && (
156+ <p className="text-[11px] text-muted-foreground" data-testid="update-release-name">
157+ <span className="font-medium">{t('simpleUpdateModal.releaseName')}</span>{' '}
158+ {releaseName}
159+ </p>
160+ )}
161+ {releaseNotes && (
162+ <div className="space-y-1">
163+ <p className="text-[11px] font-medium text-muted-foreground">
164+ {t('simpleUpdateModal.changes')}
165+ </p>
166+ <p
167+ className="text-xs text-foreground whitespace-pre-wrap max-h-28 overflow-y-auto pr-1 leading-relaxed"
168+ data-testid="update-release-notes"
169+ >
170+ {releaseNotes}
171+ </p>
172+ </div>
173+ )}
174+ </div>
175+ )}
176+
93177 {/* Restarting overlay */}
94178 {updater.state.isRestarting && (
95179 <div className="space-y-1.5">
@@ -106,7 +190,9 @@ export function SimpleUpdateModal({
106190 )}
107191
108192 {/* Download progress */}
109- {updater.state.isDownloading && !updater.state.isRestarting && (
193+ {updater.state.isDownloading &&
194+ !updater.state.isInstalling &&
195+ !updater.state.isRestarting && (
110196 <div className="space-y-1.5">
111197 <div className="flex items-center gap-2 text-xs">
112198 <Download className="w-3.5 h-3.5 animate-bounce text-foreground" />
@@ -120,6 +206,18 @@ export function SimpleUpdateModal({
120206 variant="default"
121207 />
122208 </div>
209+ )}
210+
211+ {/* Installing state */}
212+ {updater.state.isInstalling && !updater.state.isRestarting && (
213+ <div className="space-y-1.5">
214+ <div className="flex items-center gap-2 text-xs">
215+ <LoadingSpinner size="xs" variant="default" />
216+ <span className="text-muted-foreground">
217+ {t('simpleUpdateModal.installing')}
218+ </span>
219+ </div>
220+ </div>
123221 )}
124222
125223 {/* Error display */}
@@ -129,14 +227,29 @@ export function SimpleUpdateModal({
129227 <AlertTriangle className="w-3.5 h-3.5" />
130228 <span>{t('simpleUpdateModal.errorOccurred', { error: localizedError })}</span>
131229 </div>
230+ <p className="mt-2 text-[11px] text-muted-foreground">
231+ {t('simpleUpdateModal.failureGuide')}
232+ </p>
233+ <Button
234+ variant="outline"
235+ size="sm"
236+ className="mt-2 w-full"
237+ onClick={handleReportIssue}
238+ >
239+ {t('simpleUpdateModal.reportIssue')}
240+ </Button>
132241 </div>
133242 )}
134243 </div>
135244
136245 <DialogFooter className="flex-col gap-2">
137246 <Button
138247 onClick={handleDownload}
139- disabled={updater.state.isDownloading || updater.state.isRestarting}
248+ disabled={
249+ updater.state.isDownloading ||
250+ updater.state.isInstalling ||
251+ updater.state.isRestarting
252+ }
140253 size="sm"
141254 className="w-full"
142255 >
@@ -145,6 +258,11 @@ export function SimpleUpdateModal({
145258 <RotateCw className="w-3.5 h-3.5 animate-spin" />
146259 {t('simpleUpdateModal.restartingShort')}
147260 </>
261+ ) : updater.state.isInstalling ? (
262+ <>
263+ <LoadingSpinner size="xs" variant="default" />
264+ {t('simpleUpdateModal.installingShort')}
265+ </>
148266 ) : updater.state.isDownloading ? (
149267 <>
150268 <LoadingSpinner size="xs" variant="default" />
@@ -163,7 +281,11 @@ export function SimpleUpdateModal({
163281 variant="outline"
164282 size="sm"
165283 onClick={() => void handleRemindLater()}
166- disabled={updater.state.isDownloading || updater.state.isRestarting}
284+ disabled={
285+ updater.state.isDownloading ||
286+ updater.state.isInstalling ||
287+ updater.state.isRestarting
288+ }
167289 className="flex-1 text-xs"
168290 >
169291 {t('simpleUpdateModal.remindLater')}
@@ -172,7 +294,11 @@ export function SimpleUpdateModal({
172294 variant="outline"
173295 size="sm"
174296 onClick={() => void handleSkipVersion()}
175- disabled={updater.state.isDownloading || updater.state.isRestarting}
297+ disabled={
298+ updater.state.isDownloading ||
299+ updater.state.isInstalling ||
300+ updater.state.isRestarting
301+ }
176302 className="flex-1 text-xs"
177303 >
178304 {t('simpleUpdateModal.skipVersion')}
@@ -181,7 +307,11 @@ export function SimpleUpdateModal({
181307 variant="outline"
182308 size="icon-sm"
183309 onClick={onClose}
184- disabled={updater.state.isDownloading || updater.state.isRestarting}
310+ disabled={
311+ updater.state.isDownloading ||
312+ updater.state.isInstalling ||
313+ updater.state.isRestarting
314+ }
185315 aria-label={t('simpleUpdateModal.close')}
186316 >
187317 <X className="w-3.5 h-3.5" />
0 commit comments