Skip to content

Commit 5dc5783

Browse files
fix update notifications and schedule periodic packaged checks
1 parent 2104691 commit 5dc5783

5 files changed

Lines changed: 113 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
3535
`/download` now scans recent releases for the newest available `.dmg`.
3636
- Release automation now uploads `latest-mac.yml` and `.dmg.blockmap` metadata
3737
assets required by in-app auto-updates.
38+
- Packaged builds now re-check for updates periodically after startup, and the
39+
in-app update banner appears as soon as an update is available/downloading.
3840

3941
## [1.1.0] - 2026-02-15
4042

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent-observer",
3-
"version": "1.2.1",
3+
"version": "1.2.4",
44
"description": "Agent Office — 3D observability for AI agents",
55
"main": "./out/main/index.js",
66
"scripts": {

src/main/app-updates.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ const GITHUB_OWNER = 'webrenew'
88
const GITHUB_REPO = 'agent-observer'
99
const UPDATE_CHECK_TIMEOUT_MS = 7_000
1010
const UPDATE_CHECK_CACHE_MS = 30 * 60 * 1_000
11+
const PACKAGED_UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1_000
1112

1213
let handlersRegistered = false
1314
let cachedStatus: AppUpdateStatusResult | null = null
1415
let cacheTimestampMs = 0
1516
let pendingStatusPromise: Promise<AppUpdateStatusResult> | null = null
1617
let liveStatus: AppUpdateStatusResult = baseStatus(getAppVersion())
1718
let updaterInitialized = false
18-
let updaterCheckStarted = false
1919
let updaterCheckPromise: Promise<void> | null = null
20+
let lastPackagedUpdateCheckStartedAtMs = 0
21+
let periodicPackagedUpdateCheckTimer: ReturnType<typeof setInterval> | null = null
2022

2123
function getAppVersion(): string {
2224
try {
@@ -51,6 +53,14 @@ export function __testOnlyIsUpdateAvailable(currentVersion: string, latestVersio
5153
return __testOnlyCompareSemver(latestVersion, currentVersion) > 0
5254
}
5355

56+
export function __testOnlyShouldStartPackagedUpdateCheck(
57+
nowMs: number,
58+
lastCheckStartedAtMs: number,
59+
checkIntervalMs: number
60+
): boolean {
61+
return (nowMs - lastCheckStartedAtMs) >= checkIntervalMs
62+
}
63+
5464
function baseStatus(currentVersion: string): AppUpdateStatusResult {
5565
return {
5666
currentVersion,
@@ -257,11 +267,27 @@ function initializeAutoUpdaterIfNeeded(): void {
257267
downloadPercent: null,
258268
})
259269
})
270+
271+
if (!periodicPackagedUpdateCheckTimer) {
272+
periodicPackagedUpdateCheckTimer = setInterval(() => {
273+
ensureAutoUpdaterCheckStarted()
274+
}, PACKAGED_UPDATE_CHECK_INTERVAL_MS)
275+
periodicPackagedUpdateCheckTimer.unref?.()
276+
}
260277
}
261278

262-
function ensureAutoUpdaterCheckStarted(): void {
263-
if (updaterCheckStarted || updaterCheckPromise) return
264-
updaterCheckStarted = true
279+
function ensureAutoUpdaterCheckStarted(options?: { force?: boolean }): void {
280+
if (updaterCheckPromise) return
281+
const now = Date.now()
282+
if (
283+
!options?.force
284+
&& !__testOnlyShouldStartPackagedUpdateCheck(
285+
now,
286+
lastPackagedUpdateCheckStartedAtMs,
287+
PACKAGED_UPDATE_CHECK_INTERVAL_MS
288+
)
289+
) return
290+
lastPackagedUpdateCheckStartedAtMs = now
265291
updaterCheckPromise = autoUpdater.checkForUpdates()
266292
.then((result) => {
267293
if (!result?.updateInfo) {
@@ -287,7 +313,6 @@ function ensureAutoUpdaterCheckStarted(): void {
287313
}
288314
})
289315
.catch((err) => {
290-
updaterCheckStarted = false
291316
publishStatus({
292317
phase: 'error',
293318
error: normalizeUpdateError(err),
@@ -333,4 +358,9 @@ export function setupUpdateHandlers(): void {
333358
return false
334359
}
335360
})
361+
362+
if (app.isPackaged) {
363+
initializeAutoUpdaterIfNeeded()
364+
ensureAutoUpdaterCheckStarted({ force: true })
365+
}
336366
}

src/renderer/App.tsx

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ export function App() {
8383
updateStatus?.releaseUrl || 'https://github.com/webrenew/agent-observer/releases/latest'
8484
), [updateStatus?.releaseUrl])
8585

86+
const openUpdateDownloadPage = useCallback(() => {
87+
window.open(updateDownloadUrl, '_blank', 'noopener,noreferrer')
88+
}, [updateDownloadUrl])
89+
8690
const handleInstallUpdate = useCallback(() => {
8791
if (installingUpdate) return
8892
setInstallingUpdate(true)
@@ -91,15 +95,36 @@ export function App() {
9195
const accepted = await window.electronAPI.updates.installAndRestart()
9296
if (!accepted) {
9397
setInstallingUpdate(false)
94-
window.open(updateDownloadUrl, '_blank', 'noopener,noreferrer')
98+
openUpdateDownloadPage()
9599
}
96100
} catch (err) {
97101
setInstallingUpdate(false)
98102
console.warn('[App] install update failed:', err)
99-
window.open(updateDownloadUrl, '_blank', 'noopener,noreferrer')
103+
openUpdateDownloadPage()
100104
}
101105
})()
102-
}, [installingUpdate, updateDownloadUrl])
106+
}, [installingUpdate, openUpdateDownloadPage])
107+
108+
const normalizedLatestVersion = updateStatus?.latestVersion
109+
? updateStatus.latestVersion.replace(/^v/i, '')
110+
: null
111+
112+
const updateBannerMessage = useMemo(() => {
113+
if (!updateStatus) return null
114+
const versionSuffix = normalizedLatestVersion ? ` (v${normalizedLatestVersion})` : ''
115+
if (updateStatus.canInstall) return `Update ready${versionSuffix}.`
116+
if (!updateStatus.updateAvailable) return null
117+
118+
if (updateStatus.phase === 'downloading') {
119+
const progress = updateStatus.downloadPercent
120+
const progressText = typeof progress === 'number' && Number.isFinite(progress)
121+
? ` ${Math.round(progress)}%`
122+
: ''
123+
return `Update available${versionSuffix}. Downloading…${progressText}`
124+
}
125+
126+
return `Update available${versionSuffix}. Downloading…`
127+
}, [normalizedLatestVersion, updateStatus])
103128

104129
return (
105130
<div className="w-full h-full" style={{ display: 'flex', flexDirection: 'column' }}>
@@ -108,7 +133,7 @@ export function App() {
108133
<WorkspaceLayout />
109134
</ErrorBoundary>
110135
</div>
111-
{updateStatus?.canInstall ? (
136+
{updateBannerMessage ? (
112137
<div
113138
style={{
114139
position: 'fixed',
@@ -127,27 +152,44 @@ export function App() {
127152
fontSize: 12,
128153
}}
129154
>
130-
<span>
131-
Update ready{updateStatus.latestVersion ? ` (v${updateStatus.latestVersion.replace(/^v/i, '')})` : ''}.
132-
</span>
133-
<button
134-
type="button"
135-
onClick={handleInstallUpdate}
136-
disabled={installingUpdate}
137-
style={{
138-
border: '1px solid rgba(84, 140, 90, 0.6)',
139-
background: installingUpdate ? '#2a2f2a' : '#1E2920',
140-
color: installingUpdate ? '#9A9692' : '#d8dfd3',
141-
borderRadius: 6,
142-
fontSize: 11,
143-
fontWeight: 600,
144-
padding: '6px 10px',
145-
cursor: installingUpdate ? 'default' : 'pointer',
146-
opacity: installingUpdate ? 0.75 : 1,
147-
}}
148-
>
149-
{installingUpdate ? 'Restarting…' : 'Install update and restart'}
150-
</button>
155+
<span>{updateBannerMessage}</span>
156+
{updateStatus?.canInstall ? (
157+
<button
158+
type="button"
159+
onClick={handleInstallUpdate}
160+
disabled={installingUpdate}
161+
style={{
162+
border: '1px solid rgba(84, 140, 90, 0.6)',
163+
background: installingUpdate ? '#2a2f2a' : '#1E2920',
164+
color: installingUpdate ? '#9A9692' : '#d8dfd3',
165+
borderRadius: 6,
166+
fontSize: 11,
167+
fontWeight: 600,
168+
padding: '6px 10px',
169+
cursor: installingUpdate ? 'default' : 'pointer',
170+
opacity: installingUpdate ? 0.75 : 1,
171+
}}
172+
>
173+
{installingUpdate ? 'Restarting…' : 'Install update and restart'}
174+
</button>
175+
) : (
176+
<button
177+
type="button"
178+
onClick={openUpdateDownloadPage}
179+
style={{
180+
border: '1px solid rgba(84, 140, 90, 0.6)',
181+
background: '#1E2920',
182+
color: '#d8dfd3',
183+
borderRadius: 6,
184+
fontSize: 11,
185+
fontWeight: 600,
186+
padding: '6px 10px',
187+
cursor: 'pointer',
188+
}}
189+
>
190+
Open release
191+
</button>
192+
)}
151193
</div>
152194
) : null}
153195
{isSettingsOpen ? (

tests/smoke/app-updates.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'
22
import {
33
__testOnlyCompareSemver,
44
__testOnlyIsUpdateAvailable,
5+
__testOnlyShouldStartPackagedUpdateCheck,
56
} from '../../src/main/app-updates'
67

78
test('semver comparison handles v-prefix and major/minor/patch ordering', () => {
@@ -25,3 +26,10 @@ test('update availability is true only when latest is newer than current', () =>
2526
expect(__testOnlyIsUpdateAvailable('not-semver', '1.2.0')).toBe(false)
2627
expect(__testOnlyIsUpdateAvailable('1.2.0', 'not-semver')).toBe(false)
2728
})
29+
30+
test('packaged updater cadence helper enforces interval boundary', () => {
31+
expect(__testOnlyShouldStartPackagedUpdateCheck(30_000, 0, 30_000)).toBe(true)
32+
expect(__testOnlyShouldStartPackagedUpdateCheck(29_999, 0, 30_000)).toBe(false)
33+
expect(__testOnlyShouldStartPackagedUpdateCheck(100_000, 75_000, 30_000)).toBe(false)
34+
expect(__testOnlyShouldStartPackagedUpdateCheck(105_000, 75_000, 30_000)).toBe(true)
35+
})

0 commit comments

Comments
 (0)