Repository navigation
feat: in-app auto-update via electron-updater - #46
Conversation
…all UX) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s tab Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… checks, live deep link Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 58 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughAdds an Electron auto-update system with packaged background checks, user-controlled download/install actions, IPC state propagation, update settings UI, a dismissible panel banner, analytics, persistence, navigation deep links, tests, and implementation documentation. ChangesIn-app auto-update
Estimated code review effort: 4 (Complex) | ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-07-22-auto-update.md`:
- Around line 1178-1182: Synchronize the mounted Settings view with changes to
settingsLandingTab: update the SettingsView tab state after applyNav changes the
landing tab, or remount SettingsView when that value changes. Ensure clicking a
live deep-link banner switches the visible tab to Updates, while retaining
initialTab as the initial fallback for the existing scanning view.
- Around line 559-572: Update UpdaterController.check() to serialize update
checks by returning when a check is already in progress, and also return while
status.phase is 'available' so a known update remains available until
downloaded. Preserve the existing translocation handling and the guards for
'downloading' and 'downloaded', and ensure the in-progress state is cleared when
the check resolves or fails.
- Around line 575-581: Update the download() method to make repeated clicks
idempotent while an update download is pending: transition status.phase to
downloading immediately, or add an explicit in-flight guard before emitting
update_download_clicked and calling updater.downloadUpdate(). Preserve the
existing available-state check and error-event handling, and ensure the guard is
cleared if the download fails before progress begins.
In `@docs/superpowers/specs/2026-07-22-auto-update-design.md`:
- Line 93: Update the wording in the manual test checklist description to say
“requires a signed app” instead of “needs signed app,” preserving the
surrounding meaning and formatting.
In `@src/main/ipc/register-ipc.test.ts`:
- Around line 257-263: Extend the test “exposes updater state and forwards
updater actions” to invoke IPC.updaterCheck and IPC.updaterInstall, then assert
ctx.updater.check and ctx.updater.quitAndInstall are each called once, alongside
the existing getState and download coverage.
In `@src/main/updater/note-version-change.ts`:
- Around line 21-29: Update the error path in the previous !== current
persistence block of the note-version-change function so a mkdirSync or
writeFileSync failure returns null immediately after logging. Add a regression
test verifying that persistence failure does not return the previous version or
report an update, while preserving the existing return behavior when persistence
succeeds.
In `@src/main/updater/updater-service.ts`:
- Around line 98-103: Update download() in src/main/updater/updater-service.ts
(lines 98-103) to transition status to downloading with percent 0 before
emitting update_download_clicked or calling updater.downloadUpdate(). In
src/main/updater/updater-service.test.ts (lines 84-94), call download() twice
before progress and assert only one updater call and one analytics event.
In `@src/renderer/src/components/UpdateBanner/UpdateBanner.tsx`:
- Around line 23-72: Replace the clickable banner div with a primary
type="button" element so it can receive focus and activate the settings-updates
deep link via Enter/Space. Preserve the existing styling and click behavior, and
keep the dismiss button as a sibling rather than nesting it inside the primary
button; update the surrounding structure in the UpdateBanner component
accordingly.
In `@src/renderer/src/hooks/useUpdater.ts`:
- Around line 15-20: Update the useUpdater effect so the initial getUpdaterState
result is applied only if no newer onUpdaterState event has been received while
it was pending. Track whether the subscription callback has delivered an update
before applying the snapshot, while preserving the existing alive cleanup guard
and unsubscribe behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 64a1cc54-5b11-4366-a49b-c98352937e77
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (41)
STATUS.htmldocs/superpowers/plans/2026-07-22-auto-update.mddocs/superpowers/specs/2026-07-22-auto-update-design.mdpackage.jsonsrc/main/analytics/analytics.tssrc/main/app-context.types.tssrc/main/index.tssrc/main/ipc/register-ipc.test.tssrc/main/ipc/register-ipc.tssrc/main/settings/validate-setting.test.tssrc/main/settings/validate-setting.tssrc/main/updater/index.tssrc/main/updater/note-version-change.test.tssrc/main/updater/note-version-change.tssrc/main/updater/updater-logic.test.tssrc/main/updater/updater-logic.tssrc/main/updater/updater-service.test.tssrc/main/updater/updater-service.tssrc/preload/api.types.tssrc/preload/index.tssrc/renderer/src/components/UpdateBanner/UpdateBanner.constants.test.tssrc/renderer/src/components/UpdateBanner/UpdateBanner.constants.tssrc/renderer/src/components/UpdateBanner/UpdateBanner.tsxsrc/renderer/src/components/UpdateBanner/UpdateBanner.types.tssrc/renderer/src/components/UpdateBanner/index.tssrc/renderer/src/components/UpdateSettings/UpdateSettings.tsxsrc/renderer/src/components/UpdateSettings/UpdateSettings.types.tssrc/renderer/src/components/UpdateSettings/index.tssrc/renderer/src/hooks/useUpdater.tssrc/renderer/src/launcher/LauncherApp/LauncherApp.tsxsrc/renderer/src/launcher/LauncherApp/launcherNav.test.tssrc/renderer/src/launcher/LauncherApp/launcherNav.tssrc/renderer/src/launcher/views/SettingsView.tsxsrc/renderer/src/lib/format/format.test.tssrc/renderer/src/lib/format/format.tssrc/renderer/src/lib/format/index.tssrc/renderer/src/panel/PanelApp/PanelApp.tsxsrc/shared/ipc.constants.tssrc/shared/launcher-nav.types.tssrc/shared/settings.types.tssrc/shared/updater.types.ts
| check(): void { | ||
| if (isTranslocated(this.opts.execPath)) { | ||
| this.setStatus({ | ||
| phase: 'error', | ||
| message: 'TidyDisk is running from a translocated path.', | ||
| kind: 'translocation', | ||
| }) | ||
| return | ||
| } | ||
| // Never interrupt an in-flight or completed download with a fresh check. | ||
| if (this.status.phase === 'downloading' || this.status.phase === 'downloaded') return | ||
| void this.updater.checkForUpdates().catch(() => { | ||
| // failures surface through the 'error' event | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Serialize checks and preserve an available update.
check() can run while another check is already in progress, or while status.phase === 'available'. A later update-not-available or error then replaces the known update and removes the download action. Guard concurrent checks and retain the available update until the user downloads it.
Proposed guard
- if (this.status.phase === 'downloading' || this.status.phase === 'downloaded') return
+ if (
+ this.status.phase === 'checking' ||
+ this.status.phase === 'available' ||
+ this.status.phase === 'downloading' ||
+ this.status.phase === 'downloaded'
+ ) return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| check(): void { | |
| if (isTranslocated(this.opts.execPath)) { | |
| this.setStatus({ | |
| phase: 'error', | |
| message: 'TidyDisk is running from a translocated path.', | |
| kind: 'translocation', | |
| }) | |
| return | |
| } | |
| // Never interrupt an in-flight or completed download with a fresh check. | |
| if (this.status.phase === 'downloading' || this.status.phase === 'downloaded') return | |
| void this.updater.checkForUpdates().catch(() => { | |
| // failures surface through the 'error' event | |
| }) | |
| check(): void { | |
| if (isTranslocated(this.opts.execPath)) { | |
| this.setStatus({ | |
| phase: 'error', | |
| message: 'TidyDisk is running from a translocated path.', | |
| kind: 'translocation', | |
| }) | |
| return | |
| } | |
| // Never interrupt an in-flight or completed download with a fresh check. | |
| if ( | |
| this.status.phase === 'checking' || | |
| this.status.phase === 'available' || | |
| this.status.phase === 'downloading' || | |
| this.status.phase === 'downloaded' | |
| ) return | |
| void this.updater.checkForUpdates().catch(() => { | |
| // failures surface through the 'error' event | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-07-22-auto-update.md` around lines 559 - 572,
Update UpdaterController.check() to serialize update checks by returning when a
check is already in progress, and also return while status.phase is 'available'
so a known update remains available until downloaded. Preserve the existing
translocation handling and the guards for 'downloading' and 'downloaded', and
ensure the in-progress state is cleared when the check resolves or fails.
| download(): void { | ||
| if (this.status.phase !== 'available') return | ||
| this.opts.onEvent('update_download_clicked', { version: this.status.info.version }) | ||
| void this.updater.downloadUpdate().catch(() => { | ||
| // failures surface through the 'error' event | ||
| }) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Make downloads idempotent while pending.
The state remains available until the first progress event, so repeated clicks can invoke downloadUpdate() multiple times and emit duplicate analytics events. Transition to downloading immediately or maintain an explicit in-flight guard.
Proposed fix
if (this.status.phase !== 'available') return
+ const info = this.status.info
this.opts.onEvent('update_download_clicked', { version: this.status.info.version })
+ this.setStatus({ phase: 'downloading', info, percent: 0 })
void this.updater.downloadUpdate().catch(() => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| download(): void { | |
| if (this.status.phase !== 'available') return | |
| this.opts.onEvent('update_download_clicked', { version: this.status.info.version }) | |
| void this.updater.downloadUpdate().catch(() => { | |
| // failures surface through the 'error' event | |
| }) | |
| } | |
| download(): void { | |
| if (this.status.phase !== 'available') return | |
| const info = this.status.info | |
| this.opts.onEvent('update_download_clicked', { version: this.status.info.version }) | |
| this.setStatus({ phase: 'downloading', info, percent: 0 }) | |
| void this.updater.downloadUpdate().catch(() => { | |
| // failures surface through the 'error' event | |
| }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-07-22-auto-update.md` around lines 575 - 581,
Update the download() method to make repeated clicks idempotent while an update
download is pending: transition status.phase to downloading immediately, or add
an explicit in-flight guard before emitting update_download_clicked and calling
updater.downloadUpdate(). Preserve the existing available-state check and
error-event handling, and ensure the guard is cleared if the download fails
before progress begins.
| In the component: destructure `initialTab` and change the tab state line to | ||
|
|
||
| ```ts | ||
| const [tab, setTab] = useState<SettingsTab>(initialTab ?? 'scanning') | ||
| ``` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Update the mounted Settings view for live deep links.
initialTab is only used as the useState initializer. When the launcher is already showing Settings, applyNav() changes settingsLandingTab but SettingsView remains mounted, so clicking the banner does not switch to Updates. Remount on landing-tab changes or synchronize the tab with an effect.
Minimal remount fix
- <SettingsView initialTab={settingsLandingTab}
+ <SettingsView key={settingsLandingTab} initialTab={settingsLandingTab}Also applies to: 1486-1507
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-07-22-auto-update.md` around lines 1178 - 1182,
Synchronize the mounted Settings view with changes to settingsLandingTab: update
the SettingsView tab state after applyNav changes the landing tab, or remount
SettingsView when that value changes. Ensure clicking a live deep-link banner
switches the visible tab to Updates, while retaining initialTab as the initial
fallback for the existing scanning view.
| - PostHog: `update_available`, `update_download_clicked`, `update_installed` (fired on | ||
| next launch via persisted last-seen-version comparison), all with version props. | ||
| - Tests: vitest unit tests for state-machine transitions and banner-dismissal logic. | ||
| Real download/install can't run in CI (needs signed app) → manual test checklist in |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use “requires a signed app.”
Line 93’s “needs signed app” wording is ungrammatical.
🧰 Tools
🪛 LanguageTool
[style] ~93-~93: The double modal “needs signed” is nonstandard (only accepted in certain dialects). Consider “to be signed”.
Context: ...download/install can't run in CI (needs signed app) → manual test checklist in the P...
(NEEDS_FIXED)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/specs/2026-07-22-auto-update-design.md` at line 93, Update
the wording in the manual test checklist description to say “requires a signed
app” instead of “needs signed app,” preserving the surrounding meaning and
formatting.
Source: Linters/SAST tools
|
|
||
| it('exposes updater state and forwards updater actions', async () => { | ||
| const { ctx } = makeCtx(false) | ||
| expect(await invoke(IPC.updaterGetState)).toMatchObject({ status: { phase: 'idle' } }) | ||
| await invoke(IPC.updaterDownload) | ||
| expect(ctx.updater.download).toHaveBeenCalledOnce() | ||
| }) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Cover all updater IPC routes.
This test exercises getState and download, but not updaterCheck or updaterInstall. Add assertions for ctx.updater.check and ctx.updater.quitAndInstall so missing or miswired handlers cannot pass unnoticed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/ipc/register-ipc.test.ts` around lines 257 - 263, Extend the test
“exposes updater state and forwards updater actions” to invoke IPC.updaterCheck
and IPC.updaterInstall, then assert ctx.updater.check and
ctx.updater.quitAndInstall are each called once, alongside the existing getState
and download coverage.
| if (previous !== current) { | ||
| try { | ||
| mkdirSync(dirname(filePath), { recursive: true }) | ||
| writeFileSync(filePath, JSON.stringify({ version: current })) | ||
| } catch (err) { | ||
| console.error('Failed to persist last-run version', err) | ||
| } | ||
| } | ||
| return previous && valid(previous) && valid(current) && lt(previous, current) ? previous : null |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Do not report an update when persistence fails.
If writeFileSync fails, this function still returns the previous lower version. The next launch reads the same old version and emits update_installed again, producing duplicate analytics indefinitely. Return null from the write-failure path (and add a regression test).
Proposed fix
} catch (err) {
console.error('Failed to persist last-run version', err)
+ return null
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (previous !== current) { | |
| try { | |
| mkdirSync(dirname(filePath), { recursive: true }) | |
| writeFileSync(filePath, JSON.stringify({ version: current })) | |
| } catch (err) { | |
| console.error('Failed to persist last-run version', err) | |
| } | |
| } | |
| return previous && valid(previous) && valid(current) && lt(previous, current) ? previous : null | |
| if (previous !== current) { | |
| try { | |
| mkdirSync(dirname(filePath), { recursive: true }) | |
| writeFileSync(filePath, JSON.stringify({ version: current })) | |
| } catch (err) { | |
| console.error('Failed to persist last-run version', err) | |
| return null | |
| } | |
| } | |
| return previous && valid(previous) && valid(current) && lt(previous, current) ? previous : null |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/updater/note-version-change.ts` around lines 21 - 29, Update the
error path in the previous !== current persistence block of the
note-version-change function so a mkdirSync or writeFileSync failure returns
null immediately after logging. Add a regression test verifying that persistence
failure does not return the previous version or report an update, while
preserving the existing return behavior when persistence succeeds.
| return ( | ||
| <div | ||
| onClick={() => void window.clean.openLauncher('settings-updates')} | ||
| style={{ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: 8, | ||
| margin: '6px 10px 2px', | ||
| padding: '7px 10px', | ||
| borderRadius: 8, | ||
| background: 'var(--surface-2)', | ||
| border: '1px solid var(--hairline)', | ||
| cursor: 'pointer', | ||
| }} | ||
| > | ||
| <span style={{ width: 6, height: 6, borderRadius: 3, background: accent, flex: '0 0 auto' }} /> | ||
| <span | ||
| style={{ | ||
| fontSize: 11.5, | ||
| color: 'var(--text)', | ||
| flex: 1, | ||
| minWidth: 0, | ||
| whiteSpace: 'nowrap', | ||
| overflow: 'hidden', | ||
| textOverflow: 'ellipsis', | ||
| }} | ||
| > | ||
| {label} | ||
| </span> | ||
| {model.dismissible && ( | ||
| <button | ||
| aria-label="Dismiss" | ||
| onClick={(e) => { | ||
| e.stopPropagation() | ||
| onDismiss(info.version) | ||
| }} | ||
| style={{ | ||
| background: 'none', | ||
| border: 'none', | ||
| color: 'var(--text-dim)', | ||
| cursor: 'pointer', | ||
| fontSize: 12, | ||
| padding: '0 2px', | ||
| flex: '0 0 auto', | ||
| }} | ||
| > | ||
| ✕ | ||
| </button> | ||
| )} | ||
| </div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make the update deep link keyboard-accessible.
The clickable <div> cannot receive focus or be activated with Enter/Space. Use a primary type="button" for the banner content and retain the dismiss button as its sibling; this also avoids nesting a button inside an element exposed as another button.
Proposed structure
- <div onClick={() => void window.clean.openLauncher('settings-updates')} style={{ ... }}>
+ <div style={{ ... }}>
+ <button
+ type="button"
+ onClick={() => void window.clean.openLauncher('settings-updates')}
+ className="update-banner__open"
+ >
<span ... />
<span ...>{label}</span>
+ </button>
{model.dismissible && (
- <button
+ <button type="button"🧰 Tools
🪛 React Doctor (0.7.6)
[warning] 24-24: Keyboard users can't trigger this click handler because there's no keyboard one, so add onKeyUp, onKeyDown, or onKeyPress.
Pair onClick with a key handler so keyboard users can trigger it.
(click-events-have-key-events)
[warning] 24-24: Screen reader users can't tell this click handler is interactive because it has no role, so add a role or use a button or link.
Give clickable static elements a role, or use a button or link.
(no-static-element-interactions)
[warning] 53-53: Your users can submit the form by accident because a <button> with no type defaults to submit.
Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".
(button-has-type)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/src/components/UpdateBanner/UpdateBanner.tsx` around lines 23 -
72, Replace the clickable banner div with a primary type="button" element so it
can receive focus and activate the settings-updates deep link via Enter/Space.
Preserve the existing styling and click behavior, and keep the dismiss button as
a sibling rather than nesting it inside the primary button; update the
surrounding structure in the UpdateBanner component accordingly.
Source: Linters/SAST tools
| useEffect(() => { | ||
| let alive = true | ||
| void window.clean.getUpdaterState().then((s) => { | ||
| if (alive) setState(s) | ||
| }) | ||
| const unsubscribe = window.clean.onUpdaterState(setState) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Prevent a stale initial snapshot from overwriting a newer update event.
A state event can arrive before getUpdaterState() resolves; its older response then replaces the newer state. This can regress the UI (for example, from downloaded back to available).
Proposed fix
useEffect(() => {
let alive = true
- void window.clean.getUpdaterState().then((s) => {
- if (alive) setState(s)
+ let receivedPush = false
+ const unsubscribe = window.clean.onUpdaterState((next) => {
+ receivedPush = true
+ if (alive) setState(next)
})
- const unsubscribe = window.clean.onUpdaterState(setState)
+ void window.clean.getUpdaterState().then((snapshot) => {
+ if (alive && !receivedPush) setState(snapshot)
+ })
return () => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| let alive = true | |
| void window.clean.getUpdaterState().then((s) => { | |
| if (alive) setState(s) | |
| }) | |
| const unsubscribe = window.clean.onUpdaterState(setState) | |
| useEffect(() => { | |
| let alive = true | |
| let receivedPush = false | |
| const unsubscribe = window.clean.onUpdaterState((next) => { | |
| receivedPush = true | |
| if (alive) setState(next) | |
| }) | |
| void window.clean.getUpdaterState().then((snapshot) => { | |
| if (alive && !receivedPush) setState(snapshot) | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/src/hooks/useUpdater.ts` around lines 15 - 20, Update the
useUpdater effect so the initial getUpdaterState result is applied only if no
newer onUpdaterState event has been received while it was pending. Track whether
the subscription callback has delivered an update before applying the snapshot,
while preserving the existing alive cleanup guard and unsubscribe behavior.
- download() transitions to downloading 0% synchronously: UI reacts before the first progress event and a double-click can't start a second download - drop download-progress events whose rounded percent is unchanged (IPC spam) - repeat settings-updates deep links land on the Updates tab via a nav seq - 4xx no longer classified as network so a broken feed surfaces its message - decode & last in stripHtml (no double-decoding) - explicit repository field so app-update.yml generation is deterministic Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
TidyDisk now updates in place: silent background checks (launch + every 6 h, packaged builds only), then a fully user-driven flow — one click to download, another to restart and install.
UpdaterServicewraps electron-updater (autoDownload=false, install-on-quit after a download) behind an injectable interface; state machine unit-tested.update_available(deduped per version),update_download_clicked,update_installed.Manual test checklist (needs a signed build — CI can't do this)
pnpm releasetwice, bumping package.json version in between)update_installedfires on the relaunch (PostHog)Follow-ups (non-blocking, from final review)
🤖 Generated with Claude Code
Summary by CodeRabbit