From 457d5274da1db630a3a939476b04e0ee76462f35 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Mon, 18 May 2026 10:39:29 +0700 Subject: [PATCH] docs(plans): record menu-restart-button plan Plan + phase-01. Plan-context decisions: placement above Exit, no confirm dialog, cmd-handoff mechanism reused from update::install. --- .../phase-01-implement-restart-action.md | 122 ++++++++++++++++++ plans/260518-0945-menu-restart-button/plan.md | 41 ++++++ 2 files changed, 163 insertions(+) create mode 100644 plans/260518-0945-menu-restart-button/phase-01-implement-restart-action.md create mode 100644 plans/260518-0945-menu-restart-button/plan.md diff --git a/plans/260518-0945-menu-restart-button/phase-01-implement-restart-action.md b/plans/260518-0945-menu-restart-button/phase-01-implement-restart-action.md new file mode 100644 index 0000000..64f5395 --- /dev/null +++ b/plans/260518-0945-menu-restart-button/phase-01-implement-restart-action.md @@ -0,0 +1,122 @@ +# Phase 01 — Implement Restart Action + +## Context Links + +- Reused pattern: `src/update/install.rs:1-120` (cmd-handoff swap-and-restart). Documented in `docs/release-process.md` if it exists. +- Menu wiring reference: `src/app.rs:870-1045` (`show_context_menu`) and `src/app.rs:363-392` (`on_menu_command`). +- Mutex acquisition: `src/app.rs:152-168` (`Global\ClaudeCodeUsageBubble`). +- i18n schema: `src/i18n/mod.rs` (`LocaleStrings` struct around line 22-80). + +## Overview + +- **Priority:** Low (UX enhancement). +- **Status:** Done. Code-reviewer DONE_WITH_CONCERNS — M1 (match-arm ordering) + L3 (lock-during-save) addressed in follow-up edits. +- **Size:** ~50 LOC across 3 files (+ 8 locale TOMLs, one line each). + +## Key Insights + +- The existing mutex check rejects a second instance immediately. A naive "spawn-then-exit" races. The `cmd.exe /c timeout` handoff (1 s sleep, then `start ""`) is the simplest decoupling — same trick `update::install::begin` already uses. +- `cmd.exe` expands `%var%` in argument strings. Current `current_exe()` path containing `%` is an injection vector; reject it (existing precedent: `update::install` rejects too). +- `std::process::Command` with `creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)` ensures the helper outlives the parent silently. + +## Requirements + +### Functional + +- Right-click context menu shows a "Restart" item directly above "Exit". +- Clicking "Restart" closes the current process and a new instance starts within ~1–2 seconds, restoring tray icons and bubbles. +- No confirmation prompt. +- Item label is i18n-aware: all 8 locales get a translation. + +### Non-functional + +- No regression in mutex single-instance behavior — second instance must still be blocked if user accidentally launches manually mid-restart. +- No console window flashes during handoff. + +## Architecture + +``` +User clicks "Restart" + → WM_COMMAND with IDM_RESTART + → app::on_menu_command → app::restart_app() + → settings::save current snapshot (defensive flush) + → spawn detached cmd.exe with delayed `start ""` for current_exe + → PostQuitMessage(0) + → message loop exits → mutex released + → cmd.exe wakes up → new instance launches → acquires mutex → run() +``` + +## Related Code Files + +**Modify:** + +- `src/app.rs` — add `IDM_RESTART: u16 = 33` const (next free in the 30-39 band), match arm in `on_menu_command`, menu append in `show_context_menu` between `IDM_TOGGLE_WIDGET` row and the separator before `IDM_EXIT`, new `fn restart_app()`. +- `src/i18n/mod.rs` — add `pub restart: String,` field to `LocaleStrings` (place near `exit`). +- `src/i18n/locales/en.toml`, `de.toml`, `es.toml`, `fr.toml`, `ja.toml`, `ko.toml`, `nl.toml`, `zh-TW.toml` — add `restart = ""`. + +**Create:** none. + +**Delete:** none. + +## Implementation Steps + +1. **Add menu ID and i18n field.** + - `app.rs`: `const IDM_RESTART: u16 = 33;` (after `IDM_VERSION_ACTION`). + - `i18n/mod.rs`: add `pub restart: String,` to `LocaleStrings`. Place adjacent to `exit`. + - Add `restart = "Restart"` to `en.toml`. Translate for the other 7 locales (Vietnamese-quality is acceptable; native fluency not required for a single-word menu item). + +2. **Wire the menu entry.** + - `app.rs::show_context_menu` — between the `show_widget` append and the `MF_SEPARATOR` before `IDM_EXIT`, add `append_item(menu, IDM_RESTART, &snap.strings.restart, MENU_ITEM_FLAGS(0));`. + - Add `IDM_RESTART => restart_app(),` arm in `on_menu_command` before the `_ => {}` catch-all. + +3. **Implement `restart_app()`.** + - Persist a final settings snapshot (defensive flush). Read current state, call `settings::save(&snap)`. + - Resolve `std::env::current_exe()`. If `Err`, log error and `PostQuitMessage(0)` (degrade to plain Exit). + - Convert path to string. If it contains `%`, log error and return (refuse — matches `update::install` precedent). + - Build the cmd line: `timeout /t 1 >nul & start "" ""`. + - Spawn via `std::process::Command::new("cmd.exe")` with `.creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)` and a `raw_arg` payload `/c ""` (mirrors `install.rs:104-114`). + - On spawn success: `PostQuitMessage(0)`. On failure: log error and return (app stays running). + +4. **Verify.** + - `cargo check` — no warnings. + - Manual smoke test: build release, right-click tray, Restart, observe close + relaunch within ~2 s, mutex acquired by new instance, bubbles + tray icons rendered. + +## Todo List + +- [x] Add `IDM_RESTART` const in `app.rs`. +- [x] Add `restart: String` to `LocaleStrings` in `i18n/mod.rs`. +- [x] Update all 8 locale `.toml` files. +- [x] Append menu item in `show_context_menu`. +- [x] Add match arm in `on_menu_command` (placed before `IDM_LANG_BASE` guard per reviewer M1). +- [x] Implement `restart_app()` in `app.rs` (clone-then-save per reviewer L3). +- [x] `cargo check` clean. +- [ ] Manual smoke test on Windows (deferred to user; needs release build). + +## Success Criteria + +- Right-click tray → menu shows "Restart" between "Show widget" group and "Exit". +- Clicking it closes the process and a new one starts within 2 s with identical state (settings honored, bubble positions persisted, tray icons restored). +- No console window flashes. +- `cargo check` passes with no new warnings. +- All 8 locales include the new key (no fallback to English). + +## Risk Assessment + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Mutex race — new instance starts before old releases | Medium | 1 s `timeout` in cmd handoff; matches update module precedent. | +| `current_exe()` path contains `%` (injection) | Low | Reject, log, abort (same as `install.rs:90-94`). | +| `cmd.exe` not on PATH (broken Windows install) | Very Low | Log error, app stays running. User can Exit manually. | +| Settings not flushed before quit | Low | Explicit `settings::save()` before `PostQuitMessage`. Bubble positions already persist on drag, so worst case is a no-op. | +| User restart-spams the menu | Low | Each click queues a new cmd handoff; the timeout dedupes via mutex. Worst case: one extra instance attempt that exits immediately on `ERROR_ALREADY_EXISTS`. | + +## Security Considerations + +- `%`-in-path rejection prevents `cmd.exe` variable expansion injection. +- No user-supplied input enters the cmd line — only `std::env::current_exe()` output. +- Detached process flags prevent inherited stdio from leaking. + +## Next Steps + +- After merge: bump version (semver patch — UX addition with no API change). +- Consider an analogous restart action for the bubble's context menu (currently the bubble also fires `on_menu_command` via `WM_COMMAND`, so the same menu id works there for free). diff --git a/plans/260518-0945-menu-restart-button/plan.md b/plans/260518-0945-menu-restart-button/plan.md new file mode 100644 index 0000000..498eab4 --- /dev/null +++ b/plans/260518-0945-menu-restart-button/plan.md @@ -0,0 +1,41 @@ +# Plan: Menu Restart Button + +**Slug:** menu-restart-button +**Created:** 2026-05-18 09:45 +**Branch:** main +**Status:** Implemented (awaiting commit) + +## Goal + +Add a "Restart" entry to the tray right-click context menu, positioned directly above "Exit". Clicking it relaunches the running binary in-place without prompting for confirmation. + +## Why + +User-requested. Current flow to apply a config/locale tweak that doesn't hot-reload (or to recover after a hang) is Exit → relaunch from Start menu. A one-click restart is symmetric with Exit and avoids hunting for the binary again. + +## Phases + +| # | Title | Status | +|---|-------|--------| +| 01 | Implement Restart action | Done — [phase-01-implement-restart-action.md](phase-01-implement-restart-action.md) | + +## Key Decisions + +- **Placement:** main menu, between `Show widget` separator and `Exit`. NOT inside Settings submenu — keeps top-level discoverability. +- **No confirmation dialog.** Settings auto-save on every change (settings.rs:138-152); restart is non-destructive. +- **Mechanism:** detached `cmd.exe /c timeout /t 1 >nul & start "" ""` handoff, then `PostQuitMessage(0)`. Same pattern as `update::install::begin` minus the swap step. The 1-second wait lets the current process release `Global\ClaudeCodeUsageBubble` mutex before the new instance's `CreateMutexW` runs. +- **Reject paths containing `%`** — cmd.exe expands `%var%`, same defense the update module already uses (install.rs:90-94). + +## Dependencies + +- None. Pure Rust + existing `windows` crate features. + +## Out of Scope + +- Restart after settings change auto-trigger (would be a separate feature). +- Restart-with-args (e.g., toggle `--diagnose`). +- Cross-platform — Windows-only by design. + +## Unresolved Questions + +None.