Skip to content

Make updates download themselves and land in a banner (fixes #173) - #183

Closed
nthomsencph wants to merge 10 commits into
mainfrom
feat/auto-update-banner
Closed

nthomsencph wants to merge 10 commits into
mainfrom
feat/auto-update-banner

Conversation

@nthomsencph

Copy link
Copy Markdown
Collaborator

Fixes #173. Addresses #73.

What was wrong

Both halves of the report — "It keeps spamming me, and when i press update it breaks" — were real, and both are still in main today. Toast.tsx and AutoUpdateService.ts were last touched 2026-06-23, the day before the issue was filed.

The spam. The toast was raised with duration: Infinity and no sonner id, so every event stacked another one that never dismissed. And checkForUpdates short-circuited only on downloading/ready, never on available — so once an update was found and ignored, every background check (launch, then every 4h) ran a real check, electron-updater re-emitted update-available, and another permanent toast joined the pile.

The break. The toast's Download action called autoUpdateDownload() and discarded the IpcResponse, while downloadUpdate() rejected with 'No update available to download' whenever state !== 'available' — exactly the case when clicking any stacked toast after the first, or during an in-flight check. The click did nothing at all, with no error and no progress. Nothing consumed autoUpdate:downloadProgress either, so even a working download was silent for ~130 MB.

Approach

Rather than patch the toast, remove the need for one. Updates download on their own and install on quit, so there is no Download button left to mis-fire. A persistent banner at the foot of the sidebar carries progress and the Restart action and stays until acted on.

  • autoDownload = true, autoInstallOnAppQuit = true. A restart is cheap under 0.16.0 — task sessions live under the supervisor and re-attach — and the copy says so.
  • Five autoUpdate:* channels collapse into one autoUpdate:status carrying the whole state. The renderer keeps it in runtimeStore instead of stitching it together locally in two places.
  • The event→state mapping and the "is a check worth running" policy move into autoUpdateState.ts — pure and unit-tested, following the supervisorSession.ts precedent.
  • Checks on window focus and powerMonitor resume, both cooldown-bounded.
  • The last error is kept in the status and shown in Settings, so a failing updater is visible instead of silent. Updater events go to <userData>/logs/updater.log.
  • Settings → Updates gains last-check time, error text and a release-notes link (Changelog / release notes modal #73).
  • New ProgressBar primitive in components/ui/. UsageBar colours by threshold (amber, then red), which would paint a nearly-finished download red.

Testing

autoUpdateState.test.ts (14 cases) pins the transitions, including the two that caused #173: a re-check is refused once an update is in hand, and a late not-available cannot wipe an in-flight download. updateStatus.test.ts pins the wording — notably that a failed check no longer reads as "You're up to date". runtimeStore.test.ts gains 5 cases for the subscription, the post-check reconcile and error surfacing.

Full suite green (1009 passed), type-check and lint clean.

Not verified end to end. The updater does not initialise in --dev, and macOS notarization is currently failing with a 401, so nothing can be published to test the real download/install path. This needs the APPLE_APP_SPECIFIC_PASSWORD secret rotated first — see the release-pipeline branch. Worth a real 0.16.0 → 0.16.1 hop before trusting it.

Note on versioning

Bumps to 0.16.1 so 0.16.0 stays shippable as-is, matching the handoff's two-hop verification plan: ship 0.16.0, confirm the old updater still carries users across, then land this.

🤖 Generated with Claude Code

https://claude.ai/code/session_017gtmbWYyAsYEBiYQDnf4vy

claude and others added 10 commits September 20, 2026 04:50
Decisions taken, the Claude Code facts the plan relies on, target architecture,
spikes to run first, a phase 1 file-by-file breakdown (version floor, worktree
relocation to .claude/worktrees with a migration dialog, SupervisorService,
attach-based PTYs, hook port file, activity reconciliation, foreign sessions),
a phase 2 sketch and risks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2daUsvEQ2nJ6oftunoWpd
Ran S1-S7 against Claude Code 2.1.278: --bg starts without a prompt and the
job id is the session UUID prefix; attach works in node-pty and left-arrow
opens agent view; the dispatch env is frozen into the job and reused on
respawn; the agent_needs_input/agent_completed matchers never fire in the
session's own hooks; listing costs 0.3 s and fs.watch on ~/.claude/jobs works
as a trigger; a Dash linked worktree is not re-isolated. Migration needs a
claude rm before resuming into a moved worktree, resumes must re-pass --name,
and transcripts stay under the old path. Hook port moves to a file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2daUsvEQ2nJ6oftunoWpd
…ude/worktrees

PR 1 of the supervisor-sessions plan (docs/specs/2026-09-20-claude-code-
supervisor-sessions.md §6.1 and §6.2).

Version floor
- MIN_CLAUDE_VERSION (2.1.257) with pure parse/compare helpers in claudeCli.ts.
- The startup claude --version probe is memoised and awaitable; pty:startDirect
  awaits it and refuses with a typed UNSUPPORTED_CLI error below the floor.
- app:detectClaude returns ClaudeCliInfo (minVersion, supported, reason) and
  can re-run the probe. MainContent renders ClaudeCliGate instead of the task
  terminal on a missing or too-old CLI; the settings card shows the same state.
- PostCompact/StopFailure hook gates removed: both predate the floor.

Worktree relocation
- WorktreeService.getWorktreesDir is now <repo>/.claude/worktrees, the layout
  Claude Code assumes; ensureWorktreesDir also appends .claude/worktrees/ to
  .git/info/exclude so the main checkout stays clean. The reserve pool and the
  orphan-reserve sweep follow (the sweep also clears the legacy dir).

Migration
- tasks.previous_path records a moved task's old path. findLatestSessionId and
  token aggregation search both transcript dirs, since Claude keeps writing a
  moved session under the original cwd's encoded dir.
- WorktreeMigrationService plans and executes git worktree move per project
  (kills the task's PTYs first, lifts a leftover worktree lock, reports
  per-task failures). Offered once per launch by WorktreeMigrationModal with
  Later / Move now / Don't ask again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2daUsvEQ2nJ6oftunoWpd
State of the branch after PR 1, what changed and where, container quirks,
the manual verification pass PR 1 still needs, and a build order for PR 2
with the spike-verified facts it depends on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2daUsvEQ2nJ6oftunoWpd
Task sessions are now dispatched with `claude --bg` in the task worktree and
shown through `claude attach`, so they outlive task switches, renderer
reloads and Dash quitting. Hooks read the HookServer port from a file under
userData instead of the (supervisor-frozen) environment. The supervisor
listing is reconciled into the activity monitor and drives a new `stopped`
state plus a per-project "Other sessions" group with attach, stop, remove
and adopt-as-task. Archive stops the session, delete removes it, restart
re-dispatches with --resume. New setting: stop sessions on quit.

Also: DASH_USER_DATA_DIR / DASH_DEV_URL overrides for running a checkout
beside the installed app, README/CLAUDE.md updates, and a fix to the
migration test on macOS (/private/tmp).

Claude goes brr.. via Dash
Each project in the "Move task worktrees" dialog gets a checkbox (all on by
default). Unticked projects stay at the old location and are offered again
next launch, so a first move can be tried on one project.

Claude goes brr.. via Dash
A task row whose worktree directory exists at neither the old nor the new
location has nothing to move; it used to fail with "Worktree directory is
missing" and be offered again on every launch.

Claude goes brr.. via Dash
electron-builder up to 26.16.0 unlocked its temporary signing keychain with
the certificate password, which the current macos-latest runner rejects
(the v0.15.1 release run failed there). 26.16.1 fixes it.

docs/specs/2026-09-21-main-handoff-release-0.16.md describes what must
happen on main before v0.16.0 is pushed: prove signing on a branch, harden
the auto-updater (auto-download, install on quit, persistent banner), and
verify the 0.15.0 → 0.16.0 → 0.16.1 update chain.

Claude goes brr.. via Dash
workflow_dispatch runs this workflow on whatever branch it is triggered
from, and the release job would happily publish v<package.json version>
from there. Guard it on refs/heads/main so signing can be proven on a
branch without cutting a release.

Pin CI to the toolchain the repo actually uses: Node 24 (.nvmrc) and
pnpm 10 (the lockfile is v9.0 and local pnpm is 10.33), so a lockfile
format bump can't break a release.

Upload latest.yml from the Windows job too, so a future Windows updater
has a manifest. Narrow the manifest globs from release/*.yml to
release/latest*.yml while doing so: *.yml also matches builder-debug.yml,
which is why v0.15.0 shipped one as a release asset, and with three jobs
uploading it merge-multiple would collide on it.

Claude goes brr.. via Dash
Claude-Session: https://claude.ai/code/session_017gtmbWYyAsYEBiYQDnf4vy
Issue #173: "Your update available toast is toast. It keeps spamming me,
and when i press update it breaks." Both halves were real.

The spam: the toast was raised with duration Infinity and no sonner id,
so every event stacked another one that never dismissed; and
checkForUpdates only short-circuited on `downloading`/`ready`, never on
`available`. Once an update was found and ignored, each background check
(launch + every 4h) ran a real check, electron-updater re-emitted
update-available, and another permanent toast landed on the pile.

The break: the toast's Download action called autoUpdateDownload() and
threw the IpcResponse away, while downloadUpdate() rejected with "No
update available to download" whenever state !== 'available' — exactly
the case when clicking any stacked toast after the first, or during an
in-flight check. The click did nothing at all, silently. Nothing
consumed autoUpdate:downloadProgress either, so even a working download
showed no sign of life for ~130MB.

Rather than patch the toast, remove the need for one. Updates now
download on their own and install on quit, so there is no Download
button to mis-fire; a persistent banner at the foot of the sidebar
carries progress and the Restart action, and stays until acted on.
Under 0.16.0 a restart is cheap — task sessions live under the
supervisor and re-attach afterwards — so the copy says so.

The five granular autoUpdate:* channels collapse into one
autoUpdate:status carrying the whole state, which the renderer keeps in
runtimeStore instead of stitching together locally in two places. The
event→state mapping and the "is a check worth running" policy move into
autoUpdateState.ts, pure and unit-tested, following supervisorSession.ts.

Also: check on window focus and powerMonitor resume (cooldown-bounded),
keep the last error in the status so a failure is visible in Settings
rather than silent, log updater events to <userData>/logs/updater.log,
and show release notes (#73). Settings → Updates gains last-check time,
the error text and a release-notes link.

Claude goes brr.. via Dash
Claude-Session: https://claude.ai/code/session_017gtmbWYyAsYEBiYQDnf4vy
@nthomsencph

Copy link
Copy Markdown
Collaborator Author

Closing — landing this directly on main instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update available toast is toast

2 participants