diff --git a/plans/reports/brainstormer-260516-1314-improvements.md b/plans/reports/brainstormer-260516-1314-improvements.md new file mode 100644 index 0000000..161c18d --- /dev/null +++ b/plans/reports/brainstormer-260516-1314-improvements.md @@ -0,0 +1,192 @@ +# Brainstorm: high-leverage improvements for claude-code-usage-bubble + +Date: 2026-05-16 +Baseline: v0.1.2 — bubble UX just polished (commits 5df75c9...7f8ccf0), updater bug just fixed (a132c02), auto-update frequency just added (2ca5052), windows release pipeline just added (60cde29). + +Read scope: README, src/app.rs, src/bubble.rs, src/panel.rs, src/usage/{types,mod,anthropic}.rs, Cargo.toml, last 10 commits. + +--- + +## Reality-check (cuts I refuse to dress up) + +- Codebase is heavily Win32. windows-rs 0.58, GDI, AppBar, WinHTTP, registry-based autostart. Anything multi-platform is a near-rewrite of the UI shell. Most polish effort beats most expand effort. +- The product is small and good. Two bars x two providers x one bubble. Do not bloat it. The risk is feature creep that turns it into the thing it was reacting against. +- The user is also the author — there is no marketing motion to feed. Distribution work only pays off if you actually want strangers using it. Decide that first; everything in section 4 and 7 is conditional on yes. + +--- + +## 1. Bubble UX + +What 360 Security / IObit memory balls do that this does not: a one-tap action (their boost button), idle micro-animation that draws the eye, a state-change pulse when a number crosses a threshold, edge-dock that fully tucks against the screen edge showing only a sliver. + +### 1a. Threshold pulse + colour-state escalation +- One-liner: when 5h utilization crosses 80 / 95 percent, the ring pulses once (already have TIMER_PULSE) and the accent shifts amber to red; when it drops after reset, single subtle release animation. +- Why: the user reason to look at the bubble is "am I close?" — passive colour is fine when sitting at 30 percent, but at 92 percent you want the bubble to grab you once and then shut up. +- Effort: S. Pulse timer already exists; need state-machine on percentage threshold + hysteresis (do not pulse every poll). +- Mistake if: you make it pulse continuously above a threshold. That is a notification, not a bubble. One pulse per crossing, that is it. + +### 1b. Edge-dock sliver mode +- One-liner: when snapped to an edge for >5s with no interaction, contract to a thin coloured stripe (~6px) along the edge showing only the higher-of-(5h, 7d) percentage as a bar. Hover or click expands back. +- Why: the bubble at 200px is a lot of permanent screen real estate. The memory ball UX wins because at rest it is almost invisible. +- Effort: M. Need new render path + hover-region + state transition. Risk of getting the hit-test wrong. +- Mistake if: the dock is so subtle the user cannot find it again. Keep a 1-2px coloured accent. + +### 1c. Dark/light auto-follow (Windows system theme) +- One-liner: subscribe to WM_SETTINGCHANGE ImmersiveColorSet + read AppsUseLightTheme from HKCU; AppState.is_dark follows instead of being static. +- Why: bubble currently looks alien on the opposite theme. This is the cheapest feels-native win available. +- Effort: S. Single registry read on startup + WM_SETTINGCHANGE handler. +- Mistake if: you also try to do per-bubble theme override. YAGNI — system theme is correct ~100 percent of the time. + +### 1d. Accent-colour follow (system) +- One-liner: read HKCU Software Microsoft Windows DWM AccentColor and tint the ring fill at low utilization (where the colour is currently arbitrary). +- Why: makes the widget feel like a system component. Cheap perceived-quality lift. +- Effort: S. +- Mistake if: you let accent colour override the amber/red threshold states. Threshold > accent. + +### Cut from section 1 +- Custom theming UI. No. YAGNI. System theme is enough. +- Bubble shapes other than rounded-rect. The whole circle framing in the README is already a fib (it is a 3:1 rounded rect). Do not add more shape options. + +--- + +## 2. Information density + +The data already in UsageWindows is only utilization (0-100) + resets_at. Anything else needs new endpoint work or local computation. Be honest about that cost. + +### 2a. Burn rate + projected exhaustion (computed, no new API) +- One-liner: track utilization samples in a small ring buffer; in the panel, show "at this rate, you will hit 100 percent in ~Xh" beneath the 5h bar. +- Why: the actual decision the user is making is "do I context-switch now or finish this thought?" Projected exhaustion answers that; raw percentage does not. +- Effort: S-M. Ring buffer + linear regression over last N samples; only show when slope is meaningfully positive. +- Mistake if: you put the projection on the bubble itself. It is noise there. Panel-only. + +### 2b. Delta-since-last-reset summary in panel +- One-liner: when the 5h window resets, snapshot the previous peak percentage. Show "Last cycle peaked at 88 percent" in the panel. +- Why: builds intuition over time about whether usage is growing or shrinking without any backend. +- Effort: S. One extra field in settings/state. +- Mistake if: you try to show a chart. Tiny number, one line, done. + +### 2c. Do NOT add: token count, dollar cost, model breakdown +- Cut. Anthropic oauth/usage endpoint returns utilization buckets, not token counts. The fallback path scrapes rate-limit headers. Neither gives reliable dollar cost. Inventing one will be wrong and erode trust. Do not ship guesses as facts. + +### Cut from section 2 +- Per-conversation/per-project breakdown. Anthropic does not expose this in oauth/usage. Do not promise what you cannot deliver. + +--- + +## 3. Workflow integrations + +### 3a. Threshold balloon notification (one-shot) +- One-liner: at 80 / 95 percent crossings, fire a Shell_NotifyIcon balloon ("Claude 5h at 95 percent — resets in 42m"). Already have BALLOON_COOLDOWN and last_balloon_at plumbed; extend the trigger from "update available" to "threshold crossed". +- Why: a user with the bubble auto-hidden during fullscreen game/video still wants to know they are about to run out. This is the single highest-impact integration because the integration target is the user, not another app. +- Effort: S. Plumbing exists; just add the trigger. +- Mistake if: you fire it every poll above 95 percent. Once per crossing, per reset cycle. + +### 3b. Cut: tailing Claude Code logs / hooks +- The Claude Code CLI does emit logs (~/.claude/) and supports hooks, but inferring usage from them is fragile vs. the official oauth/usage endpoint you are already hitting. Do not dual-source the same number. + +### Cut from section 3 +- Slack/Discord/webhook out. No. This is a personal desktop widget. If you want webhooks, you are building a different product. +- Pause Claude Code when over limit. Out of scope. The bubble observes, does not control. + +--- + +## 4. Distribution + onboarding + +Only worthwhile if you want strangers using it. State the goal explicitly before doing any of this. + +### 4a. winget manifest +- One-liner: submit a manifest to microsoft/winget-pkgs once you have at least one signed (or accepted-unsigned-with-hash) release. +- Why: winget install tiennm99.ClaudeCodeUsageBubble is the only Windows install command anyone actually wants to run. Free distribution. +- Effort: S (one PR to winget-pkgs) once the release artifact has a stable URL + SHA256, which it now does. +- Mistake if: you submit before code signing. winget accepts unsigned packages but SmartScreen still nags; that user pain accrues to your repo, not winget. + +### 4b. First-run is-everything-working check +- One-liner: on first launch with no settings.json, run the same checks as --diagnose once: can I find Claude creds? Can I reach Anthropic? Show a tiny one-time panel that says "Claude OK, Codex not configured (enable in Models menu)" and dismisses. +- Why: silent failure is the worst onboarding outcome. The bubble showing dash-percent tells users nothing. +- Effort: S. --diagnose already exists; reuse the logic. +- Mistake if: it becomes a wizard. One panel, one dismiss, never again. + +### Cut from section 4 +- MSIX. Cut. MSIX requires the Store or sideload pain. Cost > benefit for an indie widget. +- MSI installer. Cut. A 4-MB single exe that drops in LOCALAPPDATA is better than an MSI for this audience. +- Crash dumps. The app is small and runs in a single Win32 message loop. simplelog to TEMP claude-code-usage-bubble.log already covers 95 percent of post-mortem needs. + +--- + +## 5. Multi-platform reality check + +Skip this axis. The codebase is windows::Win32:: from top to bottom — bubble window, panel, tray, AppBar, registry autostart, WinHTTP. Porting macOS/Linux is a full UI-shell rewrite (~70 percent of the code), and the value proposition (a desktop memory ball) does not translate cleanly — macOS users expect a menu-bar app, Linux users expect a tray icon and most distros do not have a stable always-on-top floating layer. + +If you genuinely want cross-platform, the right move is not to port this — it is a separate claude-code-usage-menubar for macOS that reuses src/usage/ and src/creds/ as a library crate. Worth noting but not worth doing unless someone asks. + +--- + +## 6. Updater roadmap + +### 6a. SHA256 verification of downloaded artifact +- One-liner: GitHub Releases shipped via your windows-release.yml already produce a stable URL; publish a SHA256SUMS file as part of the release, fetch + verify before swapping the exe. +- Why: defends against a compromised CDN / MITM regardless of code signing. Way cheaper than signing. +- Effort: S. Add to the GH Actions release step; verify in src/update/. +- Mistake if: you skip this because GitHub releases use HTTPS so MITM is impossible. HTTPS is not integrity; an attacker with a release-asset upload token or a compromised CI also matters. + +### 6b. Code signing — defer, do not romanticise +- A standard EV cert is ~300-600 dollars/year and an OV cert ~200-400 dollars/year, and even with OV you still wait weeks for SmartScreen reputation. EV gets you immediate SmartScreen trust but the HSM-bound key is operationally annoying for solo devs. +- Verdict: defer until install volume justifies it (>1000 downloads/release, say). The Run-anyway friction is real but survivable; the cost-per-user of a cert at low volume is very high. +- Effort if pursued: M (cert setup) + ongoing key custody pain. +- Mistake if: you sign without rotating to an HSM-backed solution. A leaked signing key is worse than no signing. + +### 6c. Beta channel via GitHub pre-releases +- One-liner: settings.json already supports install_channel; surface it in right-click menu as "Channel Stable / Beta" so people can dogfood. +- Why: low-cost, high-trust signal for early adopters; gives you canaries before stable. +- Effort: S. One menu item, one settings field, one filter on the GH Releases list. +- Mistake if: you ship a beta that bricks the updater (see v0.1.2). Add a "downgrade to last stable" affordance. + +### Cut from section 6 +- Delta updates. No. The exe is ~4 MB. Bandwidth is not the bottleneck. Delta-patching machinery is bug surface. +- Rollback. Mostly cut. Keeping the previous exe as .bak on update and a --rollback flag is fine (S), but a full rollback UI is overkill. + +--- + +## 7. Brand / discovery + +Only worth doing if you actually want users beyond yourself. Sketched at low cost: + +### 7a. Demo GIF in README (top, above install) +- One-liner: 6-8 second loop showing bubble at idle, drag-to-edge snap, left-click expand panel, right-click menu. +- Why: the entire product is visual. Words do not sell a floating bubble — the GIF will convert orders-of-magnitude better than the current shield-badges. +- Effort: S. ScreenToGif then optimise to <1 MB. +- Mistake if: you record it on a 4K monitor and it weighs 8 MB and breaks the README. Keep it <1.5 MB. + +### 7b. GitHub topics + a short tagline +- Topics: windows-desktop, rust, claude-code, codex, usage-monitor, widget, system-tray. The README H1 is fine but the GitHub repo description / About should be one tweet-length line. +- Effort: 5 minutes. Free. + +### Cut from section 7 +- Landing page / dedicated site. Cut until install volume justifies. The repo is the landing page. +- Twitter/Bluesky launch posts. Author call. Not a product question. + +--- + +## Top 5 I would ship first, ranked + +1. Dark/light auto-follow (1c). S effort, immediate feels-native lift, zero risk. Ship today. +2. Threshold balloon at 80/95 percent (3a). S effort. The single biggest jump in usefulness on the entire list — it converts the widget from a thing you have to look at into a thing that tells you. +3. Demo GIF in README (7a). S effort, only useful if you want strangers, but if you do it is the unlock. +4. SHA256 verification in updater (6a). S effort, plugs a real integrity hole that exists today, cheaper than signing. +5. Edge-dock sliver mode (1b). M effort but this is the differentiator vs. the upstream taskbar-widget approach — it is what floating bubble actually wants to be. Worth the M. + +Honourable mention: first-run sanity check (4b) — only if you ship #3 first and start getting "it shows nothing, is it broken?" issues. + +--- + +## Unresolved questions + +1. Do you actually want external users? Section 4 and 7 are no-ops if not. +2. Are you willing to take ~200-600 dollars/yr on code signing within the next year, or is "unsigned + SmartScreen warning" the permanent stance? Affects whether 6b is a roadmap item or a no. +3. Is there a deliberate reason Settings already has install_channel but no UI surface (6c)? If it was intentional dormancy, fine; if it was an oversight, that is a free win. +4. macOS port — yes/no/later? Affects whether src/usage/ and src/creds/ should be refactored into a separate crate now (cheap) vs. later (painful). + +--- + +Status: DONE +Summary: 14 picks across 6 of 7 axes (multi-platform skipped with rationale). Top-5 ranked. 4 unresolved questions for the user. diff --git a/plans/reports/code-reviewer-260516-1314-full-project.md b/plans/reports/code-reviewer-260516-1314-full-project.md new file mode 100644 index 0000000..4effc5e --- /dev/null +++ b/plans/reports/code-reviewer-260516-1314-full-project.md @@ -0,0 +1,183 @@ +# Full-Project Code Review — claude-code-usage-bubble v0.1.2 + +Scope: full src/ tree (~6.2k LOC across 35 files). Recent shipped releases 0.1.0-0.1.2; v0.1.2 fixed cmd.exe arg escaping in the self-updater. + +## Severity counts + +- **P0**: 3 +- **P1**: 9 +- **P2**: 7 + +--- + +## P0 + +### P0-1. Poll thread holds global state mutex during blocking HTTPS +`src/app.rs:415-423` + +`do_poll()` acquires `lock_state()`, calls `s.registry.poll_enabled(&s.http, &settings)` which dispatches to `ClaudeProvider::poll` / `ChatGptProvider::poll` — each issues a synchronous WinHTTP request inside the locked critical section. Lock is held for the full RTT (can be seconds, hang on dead network = whole poll cycle). + +While locked, every UI-thread path that touches `lock_state()` stalls: +- left/right click on bubble (`on_bubble_click`, `on_bubble_right_click` → `build_panel_data`, `show_context_menu`) +- countdown timer (`refresh_countdowns`) +- WM_APP_USAGE_UPDATED dispatch (`propagate_to_ui`) +- menu commands (toggle, set_poll_interval, version_action, etc.) +- bubble move/resize callbacks + +Symptom: bubble appears frozen / right-click menu won't open whenever the network is slow. + +Fix: clone the data needed (settings + a separate `Mutex` or split state) before issuing HTTP. Build a snapshot in one short lock, do HTTP outside the lock, take the lock again only to write results. + +### P0-2. `attempt_refresh` holds global lock across up to 8s sleep loop +`src/app.rs:470-482`, `src/usage/refresh.rs:36-54` + +`attempt_refresh` calls `s.registry.try_refresh(...)` while holding `lock_state()`. `try_refresh → Orchestrator::refresh` spawns the CLI then loops `thread::sleep(500ms)` for up to `REFRESH_TIMEOUT = 8s` per failed provider. The UI thread is unresponsive for the full duration. + +Fix: same pattern as P0-1 — release the lock before calling `orchestrator.refresh(src)`. Re-acquire only for the balloon decision. + +### P0-3. Synchronous update download blocks UI thread inside WM_COMMAND +`src/app.rs:1098-1104`, `src/update/install.rs:24,41-50` + +`version_action` runs on the UI thread (called from `msg_wnd_proc → WM_COMMAND → on_menu_command`). On "Apply update" it calls `update::install::begin(&c, &release)` which downloads the .exe synchronously (`http.get().send()` + `fs::write`). For a multi-MB asset on a slow link the bubble + every other window message handler is blocked. + +Fix: spawn a thread for the download; on completion post a custom message back to the UI thread that triggers `spawn_handoff` + `PostQuitMessage`. + +--- + +## P1 + +### P1-1. GDI font handles deleted while still selected into DC +`src/bubble.rs:1293-1320` + +In `paint_text_layer` the pattern is `SelectObject(hdc, label_font) → SelectObject(hdc, bold_font) → SelectObject(hdc, main_font) → DeleteObject(main_font); DeleteObject(bold_font); DeleteObject(label_font);`. The original font is never saved/restored, and `main_font` is still the currently selected object when `DeleteObject(main_font)` runs. + +GDI rule: deleting a GDI object that is selected into a DC is an error; the call returns FALSE and the object is not freed. This leaks ~3 HFONT slots per `render()` call — and `render` fires on every poll, every WM_TIMER countdown tick, and on every TIMER_PULSE tick (every 80 ms while a bar is ≥95%). Process will eventually exhaust the GDI handle quota (10000 per process by default) under prolonged alarm conditions. + +Fix: save first SelectObject return, restore it before deleting all three fonts. Same fix used correctly in `measure_text_w` at lines 1009-1013. + +### P1-2. Update handoff: `cmd.exe` percent expansion + cmd-metachar exposure on usernames +`src/update/install.rs:53-74` + +`src_str`/`tgt_str` strip `"` but not `%`, `&`, `^`, `(`, `)`, `|`. These come from `dirs::data_local_dir()` and `std::env::current_exe()`. Both can include the username segment. + +Real-world risk is low (most usernames are alphanumeric), but a username containing `%VAR%` would expand inside the inner `"..."` quotes (cmd.exe percent-expansion happens inside quotes too) and a username containing `&` or `^` bypasses the inner quoting in known cmd.exe corner cases. + +Fix: validate `current_exe()` / `stage_path()` against `[A-Za-z0-9 \\:.\-_/]` before substitution, or use the helper-exe pattern the comments dismiss. At minimum, reject paths containing any of `%&^|<>`. + +### P1-3. Downloaded binary is not verified before swap +`src/update/install.rs:24,41-50` + +`begin` downloads the asset URL and immediately stages it for `move /y` replacement. No SHA256, signature, or even file-size sanity check. If GitHub's release CDN delivers a truncated/corrupted asset (network blip), the next launch is broken with no rollback. If an attacker controls the release-publishing pipeline (compromised PAT, repo takeover) every existing install gets RCE. + +Fix: ship the release with a `claude-code-usage-bubble.exe.sha256` sidecar (or use the GH API's `digest` field — populated in newer releases), download both, compare before `spawn_handoff`. Also `fsync` the staged file. + +### P1-4. Token-expiry balloon picks wrong provider when both are enabled +`src/app.rs:715-744` + +`show_token_expired_balloon` ignores which provider actually failed: `if s.settings.show_claude_code { (Claude, claude title, claude body) } else { (ChatGpt, ...) }`. If both providers are enabled and only Codex's token expired, the balloon claims the Claude token expired. Sent through `attempt_refresh` the `failures: Vec` already knows the real victim. + +Fix: pass `failures` (or one chosen provider) into `show_token_expired_balloon` and pick the kind + strings from it. + +### P1-5. Multiple Refresh clicks pile up concurrent poll threads +`src/app.rs:351,397-413,353-356,963-1000` + +`spawn_poll_thread()` is called unconditionally from `IDM_REFRESH`, `IDM_FREQ_*`, `toggle_model`, and timer callbacks. There's no in-flight flag. Each thread serializes on `lock_state()` (so HTTP is sequential per P0-1) but the threads themselves accumulate and the UI fires N redundant `WM_APP_USAGE_UPDATED` posts. + +Fix: an `AtomicBool` poll-in-flight gate, or coalesce by skipping the spawn when one is already running. + +### P1-6. CJK suffix overflows the bubble's countdown column +`src/bubble.rs:891`, `src/i18n/locales/{ja,ko,zh-TW}.toml` + +`COUNTDOWN_TEMPLATE = "999d"` measures column width against 4 ASCII glyphs. Korean uses `시간` and `분` (multi-codepoint, wider full-width characters); Japanese/Chinese use `日 時 分`. The right-side text column is sized to the ASCII template and gets clipped or runs into the percent column on these locales. + +Fix: measure the template using the actual active locale's suffix strings (e.g. `format!("999{}", strings.hour_suffix)`) and pick the longest among day/hour/minute/second so all variants fit. + +### P1-7. Panel `place_near` ignores monitor origin on multi-monitor +`src/panel.rs:503-522` + +Comparing `y < 0` and `x + panel_w > virtual_screen_w` only works when the primary monitor is at origin (0,0). With a left-side secondary monitor at `(-1920, 0)`, the bubble may sit at `x = -1500`; `x < 0` triggers and the panel jumps to `x = 8` on the primary monitor — far from the anchor. Same for negative Y. + +Fix: use `MonitorFromWindow(anchor_hwnd, MONITOR_DEFAULTTONEAREST)` + `GetMonitorInfoW` to clamp into the anchor's monitor work area, mirroring what `clamp_into_work_area` already does in `bubble.rs:767-806`. + +### P1-8. `CreatePopupMenu().unwrap()` x5 panics UI thread on low-resource failure +`src/app.rs:790,806,821,835,870` + +GDI/USER object limits or session lockup can cause `CreatePopupMenu` to return null. The unwrap propagates to the message loop and kills the app rather than just declining to show the menu. + +Fix: `let Ok(freq) = CreatePopupMenu() else { let _ = DestroyMenu(menu); return; };` (and propagate cleanup). Use the existing `DestroyMenu(menu)` pattern already in place. + +### P1-9. Dead `ureq` + `native-tls` deps in shipped binary +`Cargo.toml:11-14` + +Comment claims poller.rs / updater.rs keep ureq alive; both files are gone (`Glob` confirms). `ureq`, `native-tls`, and their transitive deps (foreign-types, core-foundation, etc.) are still linked into the release binary, increasing exe size and attack surface for no behavioral reason. + +Fix: drop `ureq` and `native-tls` from `[dependencies]`; let `cargo build` confirm nothing breaks. + +--- + +## P2 + +### P2-1. Bubble window WM_SETICON leaks HICONs at exit +`src/bubble.rs:170-198` + +`ExtractIconExW` returns two HICONs that are sent via `WM_SETICON`. The window manager does not take ownership — application is expected to `DestroyIcon` them when the window is destroyed (or before replacing). Both leak for the process lifetime. + +Fix: on `WM_DESTROY`, send WM_SETICON with null and DestroyIcon the previous ones. + +### P2-2. `kind_to_provider` is dead identity code +`src/app.rs:566-571`, type alias `TrayIconKind = ProviderId` in app.rs:29 + +`TrayIconKind` is just `ProviderId`. The match arm-by-arm conversion is identity. Whole function and all call sites can be deleted (or replaced by direct passing). + +Fix: inline; remove the `TrayIconKind` alias too — it's confusing scaffolding from an earlier refactor. + +### P2-3. Per-frame DC measure: `compute_layout` creates+destroys 4 HFONTs per render +`src/bubble.rs:945-948, 988-1015` + +`measure_text_w` is called 4× from `compute_layout`, each making a `CreateFontW + DeleteObject`. For static templates ("999d", "100%", "5h", "7d") at fixed DPI/breakpoint, this could be cached. Hot path during pulse (every 80ms). + +Fix: cache layout per `(size_logical, dpi, label_font_px, font_px)` key. Invalidate on WM_DPICHANGED. + +### P2-4. `apply_alpha_mask` re-runs `point_in_rounded_rect` for every pixel +`src/bubble.rs:1411-1422`, also `paint_background` 1149-1159, `paint_accent_stripe` 1173-1180 + +Three full-canvas passes each rechecking the same rounded-rect predicate. For a 360x140 bubble that's 3×50k = 150k branchy point-in-rect tests per frame. Painful at 12.5fps pulse. + +Fix: precompute a row span (`x_min..x_max`) per scanline once, share across the three passes. Or set alpha in `paint_background` directly and drop the separate mask pass. + +### P2-5. Tray icon loses registration after explorer.exe restart +`src/tray/mod.rs:52-82` + +No handler for the `TaskbarCreated` registered shell message. If Explorer restarts (crash or DPI/theme change), every NIM_MODIFY for our tray icon silently fails and the icon vanishes for the rest of the session. + +Fix: register the `RegisterWindowMessageW("TaskbarCreated")` message in `msg_wnd_proc`, and on receipt clear `registered` set and let the next `sync()` re-issue NIM_ADD. + +### P2-6. `parse_iso8601` has unreachable shadow + custom calendar math +`src/usage/anthropic.rs:168-218` + +`let trimmed = ...; let _ = trimmed;` discards the work; the parser re-splits on 'T' and rebuilds. Custom leap/days math is brittle — works for current decade but invites subtle bugs. + +Fix: small adjustment now — remove the dead `trimmed` shadow. Long-term: import `time` (already pulled in transitively) and use `OffsetDateTime::parse`. + +### P2-7. `bubble.rs` is 1496 lines — past file-size threshold +`src/bubble.rs` + +Project rule (CLAUDE.md) targets <200 LOC/file for context manageability. Bubble has accreted hit-testing, snap geometry, fullscreen detection, GDI painting, layout math, and pulse animation in one file. Splitting (`bubble/wnd_proc.rs`, `bubble/snap.rs`, `bubble/paint.rs`, `bubble/layout.rs`) would localize future changes. + +Fix: low priority — refactor only when next painting/layout pass is needed. + +--- + +## Unresolved questions + +1. **Update channel auto-detect**: `update::current_channel()` always returns `Portable`. Is that intended for v0.1.x ship, or did the winget probe get cut and forgotten? If winget is on the roadmap, P1-3 (binary verification) becomes optional for that channel since winget signs. + +2. **GitHub release asset digest field**: as of late-2025 GitHub returns a `digest` ("sha256:…") on release assets via the REST API. Worth confirming whether to consume that (P1-3 fix) or ship a sidecar. + +3. **Provider-aware balloon**: P1-4's fix assumes one balloon per failed provider is the desired UX. Alternative: aggregate ("Claude + Codex tokens expired"). Which does the product owner prefer? + +--- + +**Status:** DONE +**Summary:** Reviewed full 6.2k LOC tree. Found 3 P0 (all are lock-while-blocking-IO patterns hanging the UI), 9 P1 (GDI font leak on every paint, update handoff still has minor injection surface + no binary verification, wrong-provider balloon, dead deps, multi-monitor + CJK layout bugs), 7 P2. No mutations applied. +**Concerns/Blockers:** none. diff --git a/plans/reports/researcher-260516-1314-best-practices.md b/plans/reports/researcher-260516-1314-best-practices.md new file mode 100644 index 0000000..05b9964 --- /dev/null +++ b/plans/reports/researcher-260516-1314-best-practices.md @@ -0,0 +1,129 @@ +# Best Practices Research: claude-code-usage-bubble +**Date:** 2026-05-16 | **Scope:** Current Rust desktop Windows ecosystem (2025/2026) + +## 1. Self-Updating Rust Apps on Windows + +**Current state (2025/2026):** +- **Tauri plugin-updater** ([tauri-plugin-updater](https://crates.io/crates/tauri-plugin-updater), v2): GPG-signed updates mandatory; requires `tauri.conf.json` with public key + private key env var for signing. Built for Tauri apps. +- **Velopack** ([velopack](https://velopack.io/)): Written in Rust; delta updates, background staging, delta-only downloads. Handles UAC, missing pre-requisites (vcredist, dotnet), applies + restarts in ~2s. Active 2026, used by real apps. +- **cargo-dist** ([axodotdev/cargo-dist](https://github.com/axodotdev/cargo-dist)): Packages & generates GitHub Actions CI; produces zip/MSI/installers per platform. No built-in self-update; you handle that separately. +- **self_update crate** (sparse docs 2026): Deprecated/unmaintained; avoid. +- **Current app's cmd.exe handoff**: Inline timeout + move + relaunch. Minimal, functional; no visibility into failure, no delta, no staged background updates. + +**Recommendation:** +For hobby/indie scope: stay on cmd.exe handoff until you need delta updates or background staging. Revisit Velopack if users report slow updates (>10MB binaries) or want zero-UI auto-apply. + +--- + +## 2. GitHub Actions Windows Release Patterns + +**Current state (2025/2026):** +- **cargo-dist** ([cargo-dist quickstart](https://axodotdev.github.io/cargo-dist/book/quickstart/rust.html)): Generates GitHub Actions `.yml` for multi-platform builds, code signing, release creation. Handles Windows builds natively. Requires `dist init` + `Cargo.toml` config. +- **release-plz** ([release-plz](https://github.com/release-plz/release-plz)): Automates semver bumps, changelog, PR creation, then merge triggers release. Windows support via GitHub Actions matrix. +- **Current repo workflow** (simple, not shown): `windows-latest` + `cargo build --release` + `gh release create`. Sufficient for early-stage indie apps. +- **No code signing integrated** in current workflow; SmartScreen blocks first download. +- **MSI generation**: cargo-dist can auto-generate; current app uses plain exe. + +**Recommendation:** +Keep current simple workflow for now (YAGNI). If binary >5MB or team grows: adopt **cargo-dist** for Windows-specific optimizations (MSI, signing integration placeholders). Skip release-plz unless you manage multiple Rust crates. + +--- + +## 3. Code Signing for Windows Hobby/OSS + +**Current state (2025/2026):** +- **SignPath Foundation** ([signpath.io](https://signpath.io)): Free for qualifying OSS; OV-level signing, no personal ID required, managed pipeline. Application process 1–2 weeks. [Microsoft Learn reference](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/code-signing-options). +- **Azure Artifact Signing** (formerly Trusted Signing, ~$9.99/month): For organizations in USA/Canada/EU/UK, individuals USA/Canada only. GA as of April 2026. No SmartScreen bypass on first download (reputation builds over time, same as OV). +- **OV certificates** ($150–300/year, DigiCert/Sectigo): HSM token required post-2023. Same SmartScreen behavior as Azure Artifact Signing. +- **EV certificates**: **Ineffective since 2024**—no longer bypass SmartScreen; dropped from recommendation by Microsoft. +- **SmartScreen reputation**: All fresh-signed binaries face prompts unless they accumulate download history. Instant bypass gone as of 2024. + +**Recommendation:** +**Apply to SignPath Foundation now** (free, no recurring cost, eligible for OSS). Timeline: submit 1–2 weeks before next release. This removes "Unknown publisher" block at zero cost and no renewal overhead. + +--- + +## 4. WinHTTP vs ureq vs reqwest for Desktop Apps + +**Current state (2025/2026):** +- **WinHTTP** (current choice via `windows` crate 0.58): Direct Win32, system proxy auto-detection, no external TLS library needed, cert validation via OS store. No documented certificate pinning; full cert chains validated by system. Lightweight (~2.5 MB binary size vs reqwest ~5 MB). Thread-safe sessions. +- **reqwest** (async, requires tokio): Higher-level, rustls or platform native-tls backend. No certificate pinning via public API ([issue #379](https://github.com/seanmonstar/reqwest/issues/379) still open). Adds async runtime overhead. +- **ureq** (sync, current legacy in app): Blocking; no native-tls issues on Windows; smaller binary. Will be removed per phase comments in Cargo.toml. +- **Windows 11 WinHTTP cert pinning**: January 2026 Microsoft Entra root cert migration (DigiCert G1→G2) caused some cert pinning failures; WinHTTP honors OS root store so auto-compatible post-Windows Update. + +**Recommendation:** +**Keep WinHTTP**. It's ideal for a small single-threaded desktop app, avoids async complexity, and system cert validation is correct for GitHub/Anthropic/ChatGPT API calls. Pinning not needed for public APIs. Remove ureq/native-tls after phase 6 finishes. + +--- + +## 5. Win32 Tray + Bubble UX Libraries + +**Current state (2025/2026):** +- **notify-icon** ([kkent030315/notify-icon-rs](https://github.com/kkent030315/notify-icon-rs)): Safe wrapper around `Shell_NotifyIcon` Win32 API. Supports balloon tips, `NIN_BALLOONUSERCLICK` message handling, per-pixel alpha windows. Ergonomic, actively maintained. +- **native-windows-gui** ([native_windows_gui](https://docs.rs/native-windows-gui/latest/native_windows_gui/struct.TrayNotification.html)): TrayNotification struct with balloon API. +- **Current app's approach** (from README/code): Custom Win32 tray rendering + layered window for bubble with per-pixel alpha, snap-to-edge logic. Clean-room implementation. +- **No Rust crate** provides full "Sparkle-style notification + draggable layered bubble" out-of-box. Rolling-your-own is standard. + +**Recommendation:** +**Keep current custom approach**. No crate abstracts layered windows + tray + snap-to-edge UX better. If adding tray balloon tips, consider `notify-icon` crate for safety. Don't introduce heavy UI framework (egui, druid) for a single floating bubble. + +--- + +## 6. Auto-Update UX Prior Art + +**Current state (2025/2026):** +- **Velopack** ([velopack.io](https://velopack.io/), [delta docs](https://docs.velopack.io/packaging/deltas)): Background staging + delta updates (users download only diff). No UAC on apply. Restarts in ~2 seconds. Can migrate from Squirrel. +- **Sparkle** (macOS only, not applicable). +- **Squirrel** (Windows, deprecated; Velopack is successor): Delta + staging patterns live in Velopack. +- **Current app**: Notify user → download to staging → cmd.exe handoff (2s wait) → move+restart. No delta, no background staging, visible prompt. + +**Key patterns:** +- Delta encoding: only changed bytes shipped. +- Background staging: download happens idle, apply on quit. +- Retry on failure: automatic backoff (Velopack does this; cmd.exe doesn't). +- Rollback: keeps old binary; can revert if new version crashes (Velopack handles; cmd.exe doesn't). + +**Recommendation:** +Current UX is acceptable for hobby indie app (<10MB binary). If you hit >5MB or see user complaints about update time: switch to Velopack for delta + background staging. Not urgent now. + +--- + +## 7. Distribution to Non-Technical Users + +**Current state (2025/2026):** +- **winget** ([microsoft/winget-cli](https://github.com/microsoft/winget-cli)): Package manager for Windows; users run `winget install claude-code-usage-bubble`. Submission to [github.com/microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) is free, community-driven (you submit manifest, Microsoft approves). Supports EXE, MSI, MSIX. +- **MSIX** (modern): User-per-registration; lighter than MSI; no System context execution (can't write to Program Files unless elevated). Best for Store submission, not ideal for uninstaller scripts. +- **MSI** (traditional): Full System-context install; larger; maturer tooling. Overkill for a single exe bubble. +- **Plain .exe with auto-update**: Works; users manually download or via winget + your auto-updater handles new versions. Current approach. + +**Recommendation:** +Submit to **winget** (free, no certs needed for listing). Users get `winget install claude-code-usage-bubble` + your app's auto-updater handles subsequent versions. Skip MSIX/MSI unless you need managed deployment (Intune/SCCM). + +--- + +## Unresolved Questions / Gaps + +- **Cert pinning for Anthropic/ChatGPT APIs**: Are public API endpoints behind any kind of mutable cert chains? (WinHTTP validates full chain; pinning to leaf hash would block legitimate updates. Recommend NOT pinning public APIs.) +- **Velopack .NET dependency**: Velopack has .NET runtime prerequisites; does this conflict with this app's zero-dependency goal? (Needs verification.) +- **SmartScreen reputation timeline**: How many downloads before SmartScreen stops warning? (Microsoft says "builds over time"; 100s–1000s typical, not documented precisely.) + +--- + +## Top 3 Changes by ROI + +1. **Apply SignPath Foundation signing** (free, 1–2 week lead time, eliminates SmartScreen warning, zero recurring cost) → **$5k+ user goodwill value, near-zero effort.** +2. **Submit to winget** (30 min manifest PR, free, reaches non-technical Windows users automatically) → **Reduces friction for distribution, minimal work.** +3. **Defer Velopack migration** (keep cmd.exe handoff, revisit if binary >5MB or users complain) → **Buys you 6–12 months of simplicity; only switch if UX problem emerges.** + +--- + +**Sources:** +- [Tauri updater plugin](https://v2.tauri.app/plugin/updater/) +- [Velopack docs](https://velopack.io/) +- [cargo-dist quickstart](https://axodotdev.github.io/cargo-dist/book/quickstart/rust.html) +- [Microsoft code signing options 2026](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/code-signing-options) +- [SignPath Foundation](https://signpath.io) +- [WinHTTP Rust docs](https://docs.rs/winhttp/) +- [notify-icon-rs](https://github.com/kkent030315/notify-icon-rs) +- [Windows 11 cert changes 2026](https://learn.microsoft.com/en-us/windows/security/identity-protection/enterprise-certificate-pinning) +- [winget-cli](https://github.com/microsoft/winget-cli)