Both the in-app restart and the auto-update install previously shelled out to cmd.exe so the new instance could wait for the old one to release the singleton mutex and the locked exe file. On some Windows configurations the `start ""` inside `cmd /c ...` can flash a console window despite CREATE_NO_WINDOW + DETACHED_PROCESS flags. The replacement spawns the child binary directly via CreateProcessW; since the main exe is built with windows_subsystem = "windows", no console is ever allocated. - New `src/update/handoff.rs` exposes `spawn_detached`, `wait_for_parent_exit`, and `cleanup_stale_old_exes`. - New CLI flags `--wait-pid <pid>` and `--updated-to <version>` parsed early in `main`; the child waits up to 5s on the parent PID via OpenProcess+WaitForSingleObject before falling through to a 3s mutex-acquisition retry. - `restart_app` and `install::begin` both spawn detached children using the new helper. - Update install now uses MoveFileExW twice (rename running exe sideways, then move staged exe into place with MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED so portable installs on non-system drives still work). Rollback restores the backup if either the swap OR the post-swap detached spawn fails, and a MessageBoxW modal surfaces the backup path if the rollback itself fails. - First launch after an auto-update shows a blue-info tray balloon "Updated to vX.Y.Z" via a new `tray::notify_info` (the existing `tray::notify` is split into `notify_warning` + `notify_info` sharing a `notify_inner`). - Startup sweeps stale `<exe>.old.<pid>` siblings left by past in-place updates. - Three new `LocaleStrings` fields translated across all 8 supported locales (en/nl/es/fr/de/ja/ko/zh-TW).
6.5 KiB
title, description, status, priority, branch, tags, blockedBy, blocks, created, createdBy, source
| title | description | status | priority | branch | tags | blockedBy | blocks | created | createdBy | source | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Silent in-app update + restart (no cmd.exe) | Replace cmd.exe handoff in update install + app restart paths with native Win32 (MoveFileExW + CreateProcessW) so no terminal window can ever flash. Add tray-balloon notification after auto-updates. | in_progress | P2 | main |
|
2026-05-21T07:43:16.332Z | ck:plan | skill |
Silent in-app update + restart (no cmd.exe)
Overview
Two paths today spawn cmd.exe /c "timeout ... & start ..." for update install (src/update/install.rs::begin) and app restart (src/app.rs::restart_app). Combination of CREATE_NO_WINDOW | DETACHED_PROCESS + inner start "" can still flash a console window on some Windows configs. This plan replaces both with native MoveFileExW + CreateProcessW (the main exe is windows_subsystem = "windows", so direct spawn never allocates a console). Also wires a tray balloon "Updated to vX.Y.Z" on first launch after an auto-update.
Brainstorm context: plans/reports/brainstormer-260521-1530-silent-update-no-cmd.md.
Supersedes the cmd.exe mechanism decision in 260518-0945-menu-restart-button (that plan picked cmd.exe deliberately, modeled on update::install; this plan replaces both call sites with the native path).
Phases
| Phase | Name | Status |
|---|---|---|
| 1 | Foundation: CLI flags + native spawn helper + mutex retry | Complete |
| 2 | Restart path: replace restart_app with native CreateProcessW | Complete |
| 3 | Update install: rename + move + native spawn | Complete |
| 4 | Cleanup + tray notification | Complete |
| 5 | Manual end-to-end verification | Pending (user-driven) |
Key contracts (must hold across plan)
| Contract | Source today | Invariant |
|---|---|---|
| Singleton mutex name | app.rs APP_MUTEX_NAME = Global\ClaudeCodeUsageBubble |
New instance must wait for parent to release before acquiring |
| Main binary subsystem | main.rs:1 #![windows_subsystem = "windows"] |
Direct spawn allocates no console |
| Asset filename | release.rs:7 claude-code-usage-bubble.exe |
Unchanged |
| SHA-256 verification | install.rs:64-73 |
Unchanged — still verified before swap |
| Settings save on shutdown | app.rs::restart_app snap+save |
Preserved in new restart helper |
Dependencies
No cross-plan dependencies. Related (superseded mechanism): 260518-0945-menu-restart-button.
Out of Scope
src/usage/refresh.rsCLI spawns (claude.cmd,codex.cmd,powershell.exe,wsl.exe) — already useCREATE_NO_WINDOW; tracked for follow-up.src/creds/wsl_bridge.rswsl.execalls — same.- Code signing / SmartScreen suppression — separate roadmap item.
- Cross-platform restart/update — Windows-only by design.
Unresolved Questions
None.
Validation Log
Session 1 — 2026-05-21
Verification Results
- Tier: Full (5 phases)
- Claims checked: 8
- Verified: 6 | Failed: 2 | Unverified: 0
- Verified:
app.rs:155mutex creation;app.rs:1378-1432restart_app bounds;install.rs:42-48--apply-updateexit-clean handler;install.rs:99-121spawn_handoff;os::to_utf16_nulexists and is in use;windows = 0.58features in Cargo.toml includeWin32_System_Threadingbut NOTWin32_Storage_FileSystem(phase 3 must add it). - Failed: (V1) phase 4 said
tray::notifyhas "one current caller" — actually 2 (app.rs:831,app.rs:859); (V2) phase 4 said i18n uses key-based template lookup with{v}placeholder — actually uses struct-field-basedLocaleStringswith TOML, no template engine.
Decisions
-
i18n approach: add 3 fields to
LocaleStrings+ translate 8 locale TOML files; use Rustformat!at call site for version substitution. Reason: existing pattern is struct-field-based viainclude_str!ofsrc/i18n/locales/{en,nl,es,fr,de,ja,ko,zh-TW}.toml. No template substitution at the loader level. → Propagated tophase-04-cleanup-tray-notification.md: Requirements, Related Code Files, Implementation Steps, Success Criteria, Risk Assessment all updated. -
Rollback failure escalation: Windows
MessageBoxW(MB_OK | MB_ICONERROR) when MoveFileExW step 10 AND best-effort revert both fail. Reason: silent failure here would orphan the user — they'd have no exe at install path. A clear modal tells them where the backup is (bubble.exe.old.<pid>). → Propagated tophase-03-update-install-rename-move-native-spawn.md: rollback table extended; imports list updated; new helpersurface_rollback_failureadded; newLocaleStringsfieldupdate_rollback_failed_bodyadded to phase 4's i18n work. -
Stuck-parent fallback: accept 8s total budget (5s
WaitForSingleObject+ 3s mutex retry), then exit cleanly. NoTerminateProcess. Reason: forcing process termination would defeat thesettings::savedefensive flush guarantee. The 8s ceiling is generous for normal Windows scheduling; if a real hang persists, user can manually kill old process via Task Manager — acceptable failure mode. → No phase file change needed. Phase 1 budget numbers (5s + 3s) already match. -
Auto-update timing: apply when check fires, no idle-window deferral. Reason: matches user's brainstorm-phase choice ("Fully silent auto-update"). Idle detection adds complexity (state tracking, defer-budget, defer-never-applies edge case) for marginal UX gain — bubble flicker during ~1s restart is acceptable. → No phase file change needed.
Whole-Plan Consistency Sweep
- Files reread:
plan.md,phase-01-…md,phase-02-…md,phase-03-…md,phase-04-…md,phase-05-…md - Decision deltas checked: 4
- Reconciled stale references: 2
- Phase 4 i18n section rewritten from key-based template to struct-field + Rust
format! - Phase 4
tray::notifycaller count corrected from "1" to "2" with explicit file:line citations
- Phase 4 i18n section rewritten from key-based template to struct-field + Rust
- Cross-phase touchpoints verified: Phase 3 adds
update_rollback_failed_bodytoLocaleStrings, Phase 4 owns the full i18n change (3 fields + 8 TOMLs) — both phases reference the same struct, consistent - Unresolved contradictions: 0