From 284be6cc247c46aa6e2bf2b1b271a5e7fafb5558 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:45:05 -0600 Subject: [PATCH] Merge pull request #52210 from helix4u/fix/desktop-update-progress-visibility fix(desktop): surface update progress lines --- apps/desktop/src/app/updates-overlay.tsx | 18 ++++++++++++ apps/desktop/src/store/updates.test.ts | 35 +++++++++++++++++++++++- apps/desktop/src/store/updates.ts | 20 ++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/updates-overlay.tsx b/apps/desktop/src/app/updates-overlay.tsx index 0c24dbb8978..b4c0f30cebc 100644 --- a/apps/desktop/src/app/updates-overlay.tsx +++ b/apps/desktop/src/app/updates-overlay.tsx @@ -382,6 +382,8 @@ function ApplyingView({ apply, isBackend }: { apply: UpdateApplyState; isBackend const u = t.updates const label = u.stages[apply.stage as DesktopUpdateStage] ?? u.stages.idle const body = isBackend ? u.applyingBodyBackend : u.applyingBody + const currentMessage = apply.message.trim() + const recentLog = apply.log.slice(-4) const percent = typeof apply.percent === 'number' && Number.isFinite(apply.percent) @@ -397,6 +399,12 @@ function ApplyingView({ apply, isBackend }: { apply: UpdateApplyState; isBackend {body} + + {currentMessage ? ( +

+ {currentMessage} +

+ ) : null}
@@ -409,6 +417,16 @@ function ApplyingView({ apply, isBackend }: { apply: UpdateApplyState; isBackend />
+ {recentLog.length > 1 ? ( +
+ {recentLog.map((entry, index) => ( +
+ {entry.message} +
+ ))} +
+ ) : null} +

{u.applyingClose}

) diff --git a/apps/desktop/src/store/updates.test.ts b/apps/desktop/src/store/updates.test.ts index 25ceda7c22f..09f89daa0da 100644 --- a/apps/desktop/src/store/updates.test.ts +++ b/apps/desktop/src/store/updates.test.ts @@ -370,6 +370,40 @@ describe('applyBackendUpdate recovery', () => { expect($backendUpdateApply.get().applying).toBe(false) }) + it('surfaces backend update action log lines while the action is running', async () => { + updateHermesSpy.mockResolvedValue({ ok: true, name: 'update', pid: 1 }) + getActionStatusSpy + .mockResolvedValueOnce({ + exit_code: null, + lines: ['Pulling updates...', 'Installing dependencies...'], + name: 'update', + pid: 1, + running: true + }) + .mockRejectedValueOnce(new Error('ECONNREFUSED')) + checkHermesUpdateSpy.mockResolvedValue({ + install_method: 'git', + current_version: '0.16.0', + behind: 0, + update_available: false, + can_apply: true, + update_command: 'hermes update', + message: null + }) + + const promise = applyBackendUpdate() + await vi.advanceTimersByTimeAsync(1500) + + expect($backendUpdateApply.get().message).toBe('Installing dependencies...') + expect($backendUpdateApply.get().log.map(entry => entry.message)).toEqual([ + 'Pulling updates...', + 'Installing dependencies...' + ]) + + await vi.advanceTimersByTimeAsync(5000) + await promise + }) + it('surfaces an error when the backend never comes back after the restart', async () => { updateHermesSpy.mockResolvedValue({ ok: true, name: 'update', pid: 1 }) getActionStatusSpy.mockRejectedValue(new Error('ECONNREFUSED')) @@ -383,4 +417,3 @@ describe('applyBackendUpdate recovery', () => { expect($backendUpdateApply.get().stage).toBe('error') }) }) - diff --git a/apps/desktop/src/store/updates.ts b/apps/desktop/src/store/updates.ts index 3297b6e9a90..45e0ad7e678 100644 --- a/apps/desktop/src/store/updates.ts +++ b/apps/desktop/src/store/updates.ts @@ -455,6 +455,25 @@ function finishBackendApply(returned: boolean): DesktopUpdateApplyResult { return { ok: false, error: 'apply-failed', message: 'Backend did not come back online.' } } +function ingestBackendActionStatus(status: Awaited>): void { + const current = $backendUpdateApply.get() + const log = status.lines + .filter(line => line.trim().length > 0) + .map(line => ({ at: Date.now(), message: line, stage: current.stage })) + .slice(-50) + const latest = log.at(-1)?.message + + if (log.length === 0 && !latest) { + return + } + + $backendUpdateApply.set({ + ...current, + log, + message: latest ?? current.message + }) +} + export async function applyBackendUpdate(): Promise { dismissNotification(UPDATE_TOAST_ID) $backendUpdateApply.set({ ...IDLE, applying: true, stage: 'prepare', message: translateNow('updates.applyStatus.preparing') }) @@ -477,6 +496,7 @@ export async function applyBackendUpdate(): Promise { await new Promise(resolve => globalThis.setTimeout(resolve, 1500)) try { last = await getActionStatus(started.name, 200) + ingestBackendActionStatus(last) } catch { // The dashboard restarts mid-update, dropping this connection — expected, not a failure. $backendUpdateApply.set({