Merge pull request #52210 from helix4u/fix/desktop-update-progress-visibility

fix(desktop): surface update progress lines
This commit is contained in:
Gille 2026-06-24 18:45:05 -06:00 committed by GitHub
parent 7157b213f5
commit 284be6cc24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 1 deletions

View file

@ -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
<DialogDescription className="text-center text-sm">
{body}
</DialogDescription>
{currentMessage ? (
<p className="max-w-lg break-words text-center text-xs leading-5 text-muted-foreground">
{currentMessage}
</p>
) : null}
</div>
<div className="h-2 overflow-hidden rounded-full bg-muted">
@ -409,6 +417,16 @@ function ApplyingView({ apply, isBackend }: { apply: UpdateApplyState; isBackend
/>
</div>
{recentLog.length > 1 ? (
<div className="max-h-24 overflow-hidden rounded-md border border-border/70 bg-muted/35 px-3 py-2 text-left font-mono text-[11px] leading-4 text-muted-foreground">
{recentLog.map((entry, index) => (
<div className="truncate" key={`${entry.at}-${index}`}>
{entry.message}
</div>
))}
</div>
) : null}
<p className="text-center text-xs text-muted-foreground">{u.applyingClose}</p>
</div>
)

View file

@ -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')
})
})

View file

@ -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<ReturnType<typeof getActionStatus>>): 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<DesktopUpdateApplyResult> {
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<DesktopUpdateApplyResult> {
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({