mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-14 02:15:52 +00:00
feat: initial port of claude-code-usage-monitor as a floating bubble
Windows-only floating, draggable circular bubble showing Claude Code and Codex usage. Derivative of CodeZeno/Claude-Code-Usage-Monitor (MIT), relicensed under Apache 2.0 with upstream attribution in NOTICE. - Ported verbatim: poller, updater, tray_icon, theme, localization, diagnose, models (~2,700 LOC) - Original: bubble (circular layered window, drag-anywhere via WM_NCHITTEST+HTCAPTION, snap-to-edge, Ctrl+Wheel resize, auto-hide on fullscreen), panel (expanded 5h/7d view), app (orchestrator, single-instance mutex, polling thread, context menu, dual-bubble lifecycle), settings (settings.json persistence) - Cargo.toml features cover Win32 GDI, HiDpi, Registry, Threading, Shell, WindowsAndMessaging, and KeyboardAndMouse
This commit is contained in:
commit
c0f3e3f860
42 files changed
+7046
No files matched your search
@@ -0,0 +1,121 @@
|
||||
# Phase 1: Bootstrap Repo
|
||||
|
||||
## Context Links
|
||||
|
||||
- Source `Cargo.toml`: `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor/Cargo.toml`
|
||||
- Source `build.rs`: `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor/build.rs`
|
||||
- Source icons: `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor/src/icons/`
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** Must-first (every other phase depends on this)
|
||||
- **Status:** pending
|
||||
- **Description:** Create the new repo's foundation: `Cargo.toml` with the right windows-rs features, `build.rs` for icon embedding, LICENSE (MIT, dual attribution), README with attribution to source, fresh icon assets, project layout.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Source uses `windows = 0.58` with 12 feature flags. New repo can **drop** `Win32_UI_Accessibility` (no more `SetWinEventHook` on TrayNotifyWnd) and **drop** `Win32_UI_Input_KeyboardAndMouse` if drag is handled via `WM_NCHITTEST` + `HTCAPTION` instead of manual `SetCapture`.
|
||||
- Source uses `ureq` + `native-tls` + `serde` + `dirs` — all keep verbatim.
|
||||
- Source release profile: `opt-level="z"`, `lto=true`, `strip=true`, `codegen-units=1`, `panic="abort"` — keep all (produces ~2MB binary).
|
||||
- Package name change: `claude-code-usage-bubble` (binary name `claude-code-usage-bubble.exe`).
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- `cargo build --release` produces a single-file `.exe`
|
||||
- `winres` embeds icon resource so the .exe has a proper Windows icon
|
||||
- README clearly attributes original repo + MIT license
|
||||
- Project compiles with no implementation yet (empty `main.rs` returning `()`)
|
||||
|
||||
### Non-functional
|
||||
- Binary size target: < 3 MB stripped
|
||||
- No Linux/Mac build (Windows-only `#![windows_subsystem]` in main)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
claude-code-usage-bubble/
|
||||
├── Cargo.toml
|
||||
├── build.rs
|
||||
├── LICENSE # MIT, with attribution clause
|
||||
├── README.md # Attribution + usage
|
||||
├── src/
|
||||
│ ├── main.rs # entry, mod declarations only
|
||||
│ └── icons/
|
||||
│ ├── icon.ico
|
||||
│ ├── 16x16.png, 32x32.png, 48x48.png, 256x256.png
|
||||
│ └── *.svg sources
|
||||
└── plans/ # this directory
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**To create:**
|
||||
- `Cargo.toml`
|
||||
- `build.rs`
|
||||
- `LICENSE`
|
||||
- `README.md`
|
||||
- `src/main.rs` (stub)
|
||||
- `src/icons/*` (placeholder copies from source; can be replaced with bubble-specific art later)
|
||||
- `.gitignore`
|
||||
|
||||
**To modify:** none (greenfield repo)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Create `Cargo.toml`** with package metadata, the same `windows-rs` features minus accessibility + keyboard/mouse:
|
||||
```toml
|
||||
[package]
|
||||
name = "claude-code-usage-bubble"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Floating bubble showing Claude Code / Codex usage on Windows"
|
||||
repository = "<set this>"
|
||||
```
|
||||
Features to include: `Win32_Foundation`, `Win32_Globalization`, `Win32_Graphics_Gdi`, `Win32_System_LibraryLoader`, `Win32_UI_Shell`, `Win32_UI_WindowsAndMessaging`, `Win32_System_Registry`, `Win32_System_Threading`, `Win32_Security`, `Win32_UI_HiDpi`.
|
||||
2. **Create `build.rs`** mirroring source `build.rs`; embed `src/icons/icon.ico` via `winres`.
|
||||
3. **Copy `src/icons/*`** from source verbatim (placeholder; designer can replace).
|
||||
4. **Write `LICENSE`** — MIT text with a header line crediting CodeZeno/Claude-Code-Usage-Monitor.
|
||||
5. **Write `README.md`** — short, includes:
|
||||
- One-paragraph what-it-is
|
||||
- Attribution: "This project ports usage-polling, updater, and tray-icon code from [CodeZeno/Claude-Code-Usage-Monitor](https://github.com/CodeZeno/Claude-Code-Usage-Monitor) (MIT)."
|
||||
- Install/run section (placeholder)
|
||||
6. **Stub `src/main.rs`**:
|
||||
```rust
|
||||
#![windows_subsystem = "windows"]
|
||||
fn main() {}
|
||||
```
|
||||
7. **Run `cargo build`** — must compile clean.
|
||||
8. **Run `cargo build --release`** — confirm binary produced, check size.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Cargo.toml with correct features
|
||||
- [ ] build.rs with winres
|
||||
- [ ] Icons copied from source
|
||||
- [ ] LICENSE with attribution
|
||||
- [ ] README.md with attribution
|
||||
- [ ] src/main.rs stub
|
||||
- [ ] .gitignore (target/, Cargo.lock for libs only — keep Cargo.lock for binaries)
|
||||
- [ ] `cargo build` succeeds
|
||||
- [ ] `cargo build --release` succeeds, binary < 3 MB
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- `cargo build --release` on Windows produces a runnable .exe with embedded icon
|
||||
- README links source repo
|
||||
- LICENSE includes attribution clause
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Low.** Pure configuration; no logic.
|
||||
- Cross-compile note: if developer is on Linux/Mac, will need MinGW or Windows machine for `winres` step. Document this in README.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- N/A in this phase.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 2: port portable modules into `src/`
|
||||
@@ -0,0 +1,124 @@
|
||||
# Phase 2: Port Portable Modules
|
||||
|
||||
## Context Links
|
||||
|
||||
- Source: `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor/src/`
|
||||
- Modules to port verbatim (or near-verbatim): `models.rs`, `diagnose.rs`, `theme.rs`, `poller.rs`, `updater.rs`, `tray_icon.rs`, `localization/*`
|
||||
- Module to trim: `native_interop.rs`
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** High (must precede phases 3-4)
|
||||
- **Status:** pending
|
||||
- **Description:** Bring over the portable subsystems from the source repo with minimal changes. These represent ~2,700 lines of working, tested code — the goal is to keep them intact and only edit what's required for the new project name and the simplified Win32 surface.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- `poller.rs` (1099 lines) is fully self-contained — depends only on `serde`, `ureq`, `native-tls`, `dirs`, plus `diagnose` and `models` from the same crate.
|
||||
- `updater.rs` (510 lines) embeds `env!("CARGO_PKG_REPOSITORY")` to resolve GitHub owner/repo automatically — no hardcoded references.
|
||||
- `tray_icon.rs` uses `WM_APP_TRAY` (`WM_APP + 3`) and `IDM_TOGGLE_WIDGET = 50`. Keep the constants; `app.rs` (phase 4) will own dispatch.
|
||||
- `native_interop.rs` is the trimming target: drop `find_taskbar`, `find_child_window`, `get_taskbar_rect`, `embed_in_taskbar`, `set_tray_event_hook`, `get_window_thread_id`, `unhook_win_event`. Keep `wide_str`, `colorref`, `Color`, timer-ID constants, custom-message constants.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- `mod models; mod diagnose; mod theme; mod poller; mod updater; mod tray_icon; mod localization; mod native_interop;` all compile against current `main.rs`
|
||||
- `cargo check` passes with zero warnings beyond unused-symbol warnings (which will resolve in phases 3-4)
|
||||
- All `pub` symbols documented above remain reachable
|
||||
|
||||
### Non-functional
|
||||
- No behavioral changes vs source — diffs limited to module boundaries
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs (stub from phase 1)
|
||||
├── models.rs COPIED
|
||||
├── diagnose.rs COPIED
|
||||
├── theme.rs COPIED
|
||||
├── poller.rs COPIED
|
||||
├── updater.rs COPIED + 1-line stub for current_install_channel
|
||||
├── tray_icon.rs COPIED
|
||||
├── native_interop.rs TRIMMED (~80 lines vs source 179)
|
||||
└── localization/
|
||||
├── mod.rs COPIED
|
||||
├── english.rs COPIED
|
||||
├── dutch.rs COPIED
|
||||
├── french.rs COPIED
|
||||
├── german.rs COPIED
|
||||
├── japanese.rs COPIED
|
||||
├── korean.rs COPIED
|
||||
├── spanish.rs COPIED
|
||||
└── traditional_chinese.rs COPIED
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**To create (copy from source):**
|
||||
- All files listed in Architecture section above.
|
||||
|
||||
**To modify after copying:**
|
||||
- `src/updater.rs` — replace `current_install_channel()` body with `InstallChannel::Portable` while keeping the rest of the function intact (preserves code for future winget enablement).
|
||||
- `src/native_interop.rs` — delete taskbar/WinEvent functions and their imports.
|
||||
- `src/main.rs` — declare modules; do NOT yet call `window::run` (window module doesn't exist yet).
|
||||
|
||||
**To delete:** none.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Copy modules verbatim:**
|
||||
```
|
||||
cp -r ../Claude-Code-Usage-Monitor/src/{models,diagnose,theme,poller,updater,tray_icon}.rs src/
|
||||
cp -r ../Claude-Code-Usage-Monitor/src/localization src/
|
||||
```
|
||||
2. **Copy & trim `native_interop.rs`:**
|
||||
- Keep: `wide_str`, `colorref`, `Color`, `TIMER_*` constants, `WM_APP_*` constants, `get_window_rect_safe`, `move_window`
|
||||
- Delete: `WS_POPUP_STYLE`, `WS_CHILD_STYLE`, `WS_CLIPSIBLINGS_STYLE` (bubble uses standard windows-rs constants), `EVENT_OBJECT_LOCATIONCHANGE`, `WINEVENT_OUTOFCONTEXT`, `find_taskbar`, `find_child_window`, `get_taskbar_rect`, `embed_in_taskbar`, `set_tray_event_hook`, `get_window_thread_id`, `unhook_win_event`
|
||||
- Drop imports for `Accessibility`, `Shell::SHAppBarMessage`/`APPBARDATA`, `Foundation::RECT` (if no longer used after trim)
|
||||
3. **Stub winget detection in `updater.rs`:**
|
||||
```rust
|
||||
pub fn current_install_channel() -> InstallChannel {
|
||||
// Bubble repo is not yet published to winget; once it is, restore the
|
||||
// is_winget_install_path probe by reading the source repo's logic.
|
||||
InstallChannel::Portable
|
||||
}
|
||||
```
|
||||
Keep `is_winget_install_path` + `winget_install_roots` + `normalize_path` as `#[allow(dead_code)]` to preserve the code path.
|
||||
4. **Update `src/main.rs`** to declare the modules:
|
||||
```rust
|
||||
#![windows_subsystem = "windows"]
|
||||
mod diagnose; mod localization; mod models; mod native_interop;
|
||||
mod poller; mod theme; mod tray_icon; mod updater;
|
||||
fn main() { /* phase-04 will wire this */ }
|
||||
```
|
||||
5. **Run `cargo check`** — expect warnings about unused public items; should be zero errors.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Copy 7 source modules + localization directory
|
||||
- [ ] Trim `native_interop.rs` to ~80 lines (drop taskbar/WinEvent helpers)
|
||||
- [ ] Stub `updater::current_install_channel`
|
||||
- [ ] Wire `mod` declarations in `main.rs`
|
||||
- [ ] `cargo check` clean (only dead-code warnings)
|
||||
- [ ] `cargo build --release` still produces a binary
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- All ported modules compile in the new crate without modification beyond what is listed above
|
||||
- No warnings about missing imports
|
||||
- Source code license headers (if any) are preserved
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Low.** Copy-with-rename operation; the trimming of `native_interop.rs` is the only judgment call.
|
||||
- Edge case: `tray_icon.rs` imports `crate::native_interop::WM_APP_TRAY` — verify constant survives the trim.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- `poller.rs` reads OAuth credentials from `~/.claude/.credentials.json` and (optionally) WSL distros. No new attack surface vs source.
|
||||
- `updater.rs` downloads .exe from GitHub. Same trust model as source. Until the new repo has releases published, this code is dormant.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 3: build the bubble window (replaces 2847 lines of `window.rs`)
|
||||
@@ -0,0 +1,166 @@
|
||||
# Phase 3: Build Floating Bubble Window
|
||||
|
||||
## Context Links
|
||||
|
||||
- Source `window.rs` painting + drag logic: `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor/src/window.rs` (lines around `UpdateLayeredWindow`, `WM_LBUTTONDOWN`, `SetCapture`)
|
||||
- Reference UX: 360 Security floating ball, IObit Advanced SystemCare RAM-boost ball — both are circular, top-most, draggable-anywhere with edge snap.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** Critical — this is the heart of the new UX
|
||||
- **Status:** pending
|
||||
- **Description:** Build a circular floating bubble window that floats on top of everything, can be dragged anywhere, snaps to monitor edges, and shows usage percentage in the center over a colored progress ring. Replaces the 2847-line `window.rs` taskbar embedding code.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Use `WS_POPUP | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW` — `TOOLWINDOW` keeps it out of Alt+Tab; `NOACTIVATE` prevents focus theft.
|
||||
- Drag-anywhere = handle `WM_NCHITTEST`, return `HTCAPTION` for the entire bubble area. The OS handles drag automatically, including with proper cursor and Win+drag behavior. No need for `SetCapture`.
|
||||
- Circular alpha mask: render to a DIB section with per-pixel alpha. Pixels outside the circle = `0x00000000` (fully transparent). Then `UpdateLayeredWindow` with `ULW_ALPHA`. Click-through outside the circle happens automatically because alpha=0 doesn't hit-test (default behavior of layered windows with `WS_EX_LAYERED` + per-pixel alpha; verify via test).
|
||||
- Snap to edge: in `WM_EXITSIZEMOVE`, query current position via `GetWindowRect`, find nearest monitor via `MonitorFromPoint`, get its work area via `GetMonitorInfo`. If center is within 12px of any work-area edge, snap that edge.
|
||||
- HiDPI: use `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)` early in `main`. Query `GetDpiForWindow` on `WM_DPICHANGED` and rescale bubble size + font.
|
||||
- GDI ring drawing: parametric — sweep angle proportional to percentage. Use `Polygon` filled with brush, or `AngleArc` with thick pen. For clean anti-aliased look on layered window, draw into DIB section manually with alpha-weighted line algorithm. Simpler path: use GDI+ via `gdiplus` crate, or accept GDI-aliased look for v1.
|
||||
- Bubble size default: **56×56 px** at 100% DPI (matches reference apps). Allow user to tweak in `Cargo.toml` constant for v1.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- Window appears as a circular bubble, can be dragged anywhere on any monitor
|
||||
- Bubble shows percentage text (e.g. "73%") in center
|
||||
- Colored progress ring around the percentage; color matches the source app's color stops (orange → red gradient from 50% to 100%)
|
||||
- Top-most: stays visible over other windows
|
||||
- No taskbar entry, no Alt-Tab entry
|
||||
- Left-click → posts `WM_APP_PANEL_TOGGLE` to self (panel implementation in phase 4)
|
||||
- Right-click → context menu (phase 4 owns menu items; bubble owns the right-click detection)
|
||||
- Drag releases → snap to nearest monitor edge if within 12 px of it
|
||||
- Repaints when percentage / theme / DPI changes
|
||||
|
||||
### Non-functional
|
||||
- 60 FPS not required; redraws on data update only (every 60s poll cycle plus countdown ticks)
|
||||
- Per-monitor DPI aware
|
||||
- Visible on dark and light Windows themes (use `theme::is_dark_mode` for ring background tint)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── bubble.rs NEW — Window class, message loop owner, GDI painting,
|
||||
│ drag + snap, DPI, hit-testing
|
||||
└── (other modules unchanged from phase 2)
|
||||
```
|
||||
|
||||
Bubble owns:
|
||||
- HWND lifecycle (`RegisterClassExW` + `CreateWindowExW`)
|
||||
- DIB section + `UpdateLayeredWindow` call
|
||||
- Percentage state (`Option<f64>` for each enabled model)
|
||||
- Drag state (managed by OS via `HTCAPTION`)
|
||||
- Snap math
|
||||
- DPI scale factor cache
|
||||
|
||||
Bubble delegates:
|
||||
- Polling → `poller::poll` (phase 4 wires the background thread)
|
||||
- Panel toggle → `app::on_panel_toggle` (phase 4)
|
||||
- Right-click menu → `app::on_show_context_menu` (phase 4)
|
||||
- Settings → `settings` module (phase 4)
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**To create:**
|
||||
- `src/bubble.rs` (target: 400–700 lines)
|
||||
|
||||
**To modify:**
|
||||
- `src/main.rs` — eventually call `bubble::run()` (wired in phase 4)
|
||||
- `src/native_interop.rs` — may add helpers if hit-testing geometry math gets gnarly
|
||||
|
||||
**To delete:** none.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Window class registration:**
|
||||
- Class name: `ClaudeCodeUsageBubble`
|
||||
- Style: `CS_DBLCLKS` (allow `WM_LBUTTONDBLCLK` if we want double-click later)
|
||||
- WndProc: `bubble_wnd_proc`
|
||||
2. **Window creation:**
|
||||
- `WS_POPUP`, ext `WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW`
|
||||
- Initial position: load from settings (phase 4); fall back to "near bottom-right corner of primary monitor"
|
||||
- Size: 56×56 logical px scaled by current DPI
|
||||
3. **DPI awareness:**
|
||||
- In `bubble::run`, call `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)`.
|
||||
- On `WM_CREATE`, cache DPI via `GetDpiForWindow`.
|
||||
- On `WM_DPICHANGED`, update scale and resize.
|
||||
4. **Painting (the hard part):**
|
||||
- On every state change (percentage, DPI, theme), call `redraw()`.
|
||||
- `redraw()`:
|
||||
- Create DIB section sized to bubble pixel dimensions (`CreateDIBSection` with `BI_RGB` and 32bpp).
|
||||
- Clear to fully transparent (`0x00000000`).
|
||||
- For each pixel inside the circle radius, write background fill (theme-adjusted: dark theme → semi-opaque dark with high alpha; light theme → semi-opaque white).
|
||||
- Stroke progress ring: for the sweep angle proportional to current percentage, draw a thick arc using either GDI `AngleArc` with rounded `Pen`, or manual pixel writes (4 px ring thickness at 100% DPI).
|
||||
- Draw percentage text in center via `DrawTextW` with `DT_CENTER | DT_VCENTER | DT_SINGLELINE`. Font: bold 14 pt at 100% DPI, scaled by DPI factor.
|
||||
- Call `UpdateLayeredWindow` with `ULW_ALPHA` and the DIB.
|
||||
5. **Drag-anywhere via `WM_NCHITTEST`:**
|
||||
```rust
|
||||
WM_NCHITTEST => {
|
||||
// Convert lparam (screen coords) to client coords
|
||||
let p = screen_to_client(hwnd, lparam);
|
||||
if inside_circle(p, radius) { LRESULT(HTCAPTION as isize) }
|
||||
else { LRESULT(HTTRANSPARENT as isize) }
|
||||
}
|
||||
```
|
||||
OS handles drag + cursor. `HTTRANSPARENT` outside the circle ensures clicks pass through.
|
||||
6. **Snap on drag release:**
|
||||
- `WM_EXITSIZEMOVE` → snap logic.
|
||||
- `MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)` → `GetMonitorInfo` → work area rect.
|
||||
- Compare bubble's center to each edge of work area. If distance < 12 logical px (scaled by DPI), adjust window position to snap.
|
||||
- Persist new position via `app::on_bubble_moved(model, x, y)` (phase 4).
|
||||
7. **Click handling:**
|
||||
- `WM_LBUTTONUP` → if no drag occurred (compare with `WM_LBUTTONDOWN` position), `PostMessageW(WM_APP_PANEL_TOGGLE)`.
|
||||
- `WM_RBUTTONUP` → call into `app::show_context_menu(hwnd, screen_pos)` (phase 4 implements).
|
||||
8. **Public API:**
|
||||
```rust
|
||||
pub fn run(initial: BubbleConfig) -> ! { /* never returns; spins message loop */ }
|
||||
pub struct BubbleConfig {
|
||||
pub model: TrayIconKind, // Claude or Codex
|
||||
pub initial_position: Option<(i32, i32)>,
|
||||
pub initial_percentage: Option<f64>,
|
||||
}
|
||||
pub fn update_percentage(hwnd: HWND, percentage: Option<f64>); // called from poll thread via PostMessage
|
||||
```
|
||||
For dual-bubble mode, phase 4 spawns one `bubble::run` per enabled model on separate threads (each with its own message loop) — simpler than juggling two HWNDs in one thread.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Window class + creation with correct styles
|
||||
- [ ] Per-monitor DPI awareness on entry
|
||||
- [ ] DIB section + layered window painting pipeline
|
||||
- [ ] Circle fill with theme-aware background
|
||||
- [ ] Progress ring painted at correct sweep angle, correct color stop
|
||||
- [ ] Percentage text drawn centered
|
||||
- [ ] `WM_NCHITTEST` returns `HTCAPTION` inside circle, `HTTRANSPARENT` outside
|
||||
- [ ] Drag works smoothly across monitors
|
||||
- [ ] Snap on release within 12px of work-area edge
|
||||
- [ ] Left-click (no drag) posts panel-toggle message
|
||||
- [ ] Right-click posts context-menu request
|
||||
- [ ] Public API `run`, `update_percentage`
|
||||
- [ ] Manual test: bubble visible, draggable, snaps, percentage updates
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Bubble appears on Windows 10/11 with a Visual Studio-clean cargo build
|
||||
- Drag works smoothly with no flicker
|
||||
- Edge snap engages reliably from 12 px
|
||||
- Bubble survives display reconnection (laptop → external monitor → unplug)
|
||||
- Percentage text remains crisp on 100%, 125%, 150%, 175% DPI
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **High** — this is novel code with no exact analog in source.
|
||||
- Risk: ClearType sub-pixel text rendering on a per-pixel-alpha layered window looks bad. Source repo's `window.rs` solved this with a black background-pixel hack (`alpha = 0x01` so it's nearly transparent but still gets ClearType). Apply the same trick for the circle's interior fill region.
|
||||
- Risk: GDI `AngleArc` doesn't anti-alias. Mitigation: either accept aliased v1, or render to a 2x supersampled DIB and downsample.
|
||||
- Risk: Snap math wrong on rotated taskbar or unusual DPI configurations. Mitigation: clamp to monitor work area only, ignore taskbar position.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- N/A in this phase; bubble does not handle user input beyond mouse position and clicks.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 4: expanded panel, settings persistence, polling thread, orchestration
|
||||
@@ -0,0 +1,190 @@
|
||||
# Phase 4: Expanded Panel + Settings Persistence + Orchestration
|
||||
|
||||
## Context Links
|
||||
|
||||
- Source `window.rs` — borrow message-loop dispatch, polling-thread orchestration, settings persistence pattern
|
||||
- Source `poller.rs` — `poll`, `credential_watch_snapshot`, `format_line`, `time_until_display_change`
|
||||
- Source `tray_icon.rs` — `add/update/remove/sync`, `handle_message`
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** High
|
||||
- **Status:** pending
|
||||
- **Description:** Build (a) the expanded panel that appears on bubble click and shows both 5h and 7d bars with countdowns, (b) settings persistence to `%APPDATA%\ClaudeCodeUsageBubble\settings.json`, (c) the orchestrating `app.rs` module that owns polling, message routing, context menus, and dual-bubble lifecycle.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Panel is a separate window: `WS_POPUP | WS_EX_LAYERED | WS_EX_TOPMOST`, opaque background, shown adjacent to bubble. Source's draw code for the horizontal bars can be ported almost directly (it already draws progress bars + countdown text via GDI).
|
||||
- Settings file location matches source pattern: `%APPDATA%\ClaudeCodeUsageBubble\settings.json` (renamed dir). Use `dirs::config_dir()`.
|
||||
- Polling: background `std::thread` spawned in `app::run`. Posts `WM_APP_USAGE_UPDATED` to each bubble window when data refreshes. Source's poll-loop logic is copy-friendly.
|
||||
- Context menu: built via `CreatePopupMenu` + `AppendMenuW` + `TrackPopupMenu`. Source has the full menu structure — port it but remove "Reset position" → rename to "Reset bubble position" (per model).
|
||||
- Dual-bubble: each enabled model gets its own HWND + tray icon + bubble window. Settings stores `bubble_positions: { claude: {x, y}, codex: {x, y} }`.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- Settings persist across restarts: window positions, polling frequency, enabled models, language, "Start with Windows" state, last update check
|
||||
- Expanded panel: shows session bar + weekly bar + countdowns + reset times for the model whose bubble was clicked
|
||||
- Panel auto-closes on focus loss or after a brief timeout (optional)
|
||||
- Right-click menu mirrors source's menu structure: Refresh, Models, Update frequency, Language, Start with Windows, Reset position, Updates, Exit
|
||||
- Single-instance enforced via named mutex `Global\ClaudeCodeUsageBubble`
|
||||
- Polling runs in background, posts updates via `PostMessageW(WM_APP_USAGE_UPDATED, ...)`
|
||||
- Countdown timer adapts to display granularity (`time_until_display_change`)
|
||||
|
||||
### Non-functional
|
||||
- Settings file is atomically written (write to `.tmp`, rename)
|
||||
- Polling thread cannot block UI thread
|
||||
- Mutex released on clean shutdown
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.rs NEW — orchestrator: spawns bubbles, polls, routes messages,
|
||||
│ owns tray icons, owns context menu builder
|
||||
├── panel.rs NEW — expanded panel window (one per model on demand)
|
||||
├── settings.rs NEW — load/save settings.json, schema
|
||||
└── main.rs modified — calls app::run
|
||||
```
|
||||
|
||||
Message flow:
|
||||
|
||||
```
|
||||
poll thread ────PostMessage(WM_APP_USAGE_UPDATED)──▶ bubble HWND
|
||||
└─▶ updates percentage, redraws
|
||||
bubble click ──PostMessage(WM_APP_PANEL_TOGGLE)─▶ app handler (in bubble wndproc)
|
||||
└─▶ panel::show_for(model)
|
||||
right-click ──app::show_context_menu(hwnd)──▶ TrackPopupMenu ─▶ WM_COMMAND
|
||||
└─▶ menu action dispatch
|
||||
tray icon ────WM_APP_TRAY───────────▶ tray_icon::handle_message ─▶ TrayAction
|
||||
└─▶ toggle/ shutdown / refresh
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**To create:**
|
||||
- `src/app.rs` (target: 500–800 lines)
|
||||
- `src/panel.rs` (target: 300–500 lines)
|
||||
- `src/settings.rs` (target: 150–250 lines)
|
||||
|
||||
**To modify:**
|
||||
- `src/main.rs`:
|
||||
```rust
|
||||
#![windows_subsystem = "windows"]
|
||||
mod app; mod bubble; mod diagnose; mod localization; mod models;
|
||||
mod native_interop; mod panel; mod poller; mod settings; mod theme;
|
||||
mod tray_icon; mod updater;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.iter().any(|a| a == "--diagnose") {
|
||||
if let Ok(path) = diagnose::init() {
|
||||
diagnose::log(format!("startup args={args:?} log_path={}", path.display()));
|
||||
}
|
||||
}
|
||||
if let Some(exit_code) = updater::handle_cli_mode(&args) {
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
app::run();
|
||||
}
|
||||
```
|
||||
|
||||
**To delete:** none.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **`settings.rs`** — define schema:
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct Settings {
|
||||
pub show_claude_code: bool, // default true
|
||||
pub show_codex: bool, // default false
|
||||
pub bubble_positions: BubblePositions,
|
||||
pub poll_minutes: u32, // default 5
|
||||
pub language: Option<String>, // None = system
|
||||
pub start_with_windows: bool,
|
||||
pub last_update_check_unix: Option<i64>,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct BubblePositions {
|
||||
pub claude: Option<(i32, i32)>,
|
||||
pub codex: Option<(i32, i32)>,
|
||||
}
|
||||
pub fn load() -> Settings { /* dirs::config_dir + read + serde + atomic */ }
|
||||
pub fn save(s: &Settings) { /* write .tmp + rename */ }
|
||||
```
|
||||
2. **`panel.rs`** — port the existing horizontal-bar painting code from source `window.rs`:
|
||||
- Window class `ClaudeCodeUsageBubblePanel`
|
||||
- On `WM_LBUTTONDOWN` outside → close
|
||||
- On `WM_KILLFOCUS` → close (with debounce so it doesn't close instantly when bubble is clicked again to toggle off)
|
||||
- Paints two rows (5h, 7d) for the model that was clicked; uses `poller::format_line` for the countdown text
|
||||
- Position: anchor next to bubble; flip side if would go off-screen
|
||||
3. **`app.rs`** — orchestrator:
|
||||
- `pub fn run() -> !`:
|
||||
1. Acquire single-instance mutex; if already running, exit.
|
||||
2. `SetProcessDpiAwarenessContext`.
|
||||
3. Load settings.
|
||||
4. Resolve language (`localization::resolve_language`).
|
||||
5. Start polling thread.
|
||||
6. For each enabled model, spawn a bubble window thread.
|
||||
7. Run main message loop on UI thread (the bubble windows can be on the same thread — easier than multi-thread message pumps).
|
||||
- Polling thread:
|
||||
```rust
|
||||
loop {
|
||||
match poller::poll(show_claude, show_codex) {
|
||||
Ok(data) => post_update_to_bubbles(data),
|
||||
Err(e) => post_error_to_app(e),
|
||||
}
|
||||
sleep(poll_interval);
|
||||
}
|
||||
```
|
||||
- Context menu builder: replicate source's menu structure verbatim; localized strings via `Strings`. Map `WM_COMMAND` IDs to handlers (refresh, toggle model, change interval, etc.).
|
||||
- "Start with Windows": registry write to `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run\ClaudeCodeUsageBubble`.
|
||||
- Token-expired flow: if `PollError::TokenExpired`, show tray balloon via `tray_icon::notify_balloon` with localized title/body.
|
||||
4. **Wire `bubble.rs` events back to `app.rs`:**
|
||||
- `WM_APP_PANEL_TOGGLE` handler in bubble wndproc → call `panel::show_for_model(model)` (registered via callback or via `app::handle_panel_toggle`).
|
||||
- `WM_RBUTTONUP` → call `app::show_context_menu_at(point, model)`.
|
||||
- `WM_EXITSIZEMOVE` → call `app::on_bubble_moved(model, x, y)` which updates settings.
|
||||
|
||||
5. **Reuse `tray_icon.rs`** as the source-app's secondary indicator:
|
||||
- In `app::run`, after creating bubbles, call `tray_icon::sync(hwnd, &[TrayIconData{kind: Claude, percent: …, tooltip: …}, …])`.
|
||||
- Tray icon clicked → `tray_icon::handle_message` → if `ToggleWidget`, toggle bubble visibility (set `WS_VISIBLE` style); if `ShowContextMenu`, dispatch to `app::show_context_menu_at`.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] `settings.rs` with atomic save
|
||||
- [ ] `panel.rs` with bar painting + auto-close
|
||||
- [ ] `app.rs` orchestrator
|
||||
- [ ] Single-instance mutex acquisition + release
|
||||
- [ ] Polling thread spawning + `PostMessage` updates
|
||||
- [ ] Context menu localized + dispatchable
|
||||
- [ ] Start-with-Windows registry roundtrip
|
||||
- [ ] Tray icons synced from polling updates
|
||||
- [ ] Bubble-to-app event callbacks wired
|
||||
- [ ] Dual-bubble mode tested (both Claude + Codex enabled)
|
||||
- [ ] `cargo build --release` → working binary
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Launching the app twice: second launch exits silently
|
||||
- Bubble + panel + tray icon all show correct usage after first poll
|
||||
- Right-click menu functional for all items
|
||||
- Toggle Claude/Codex via menu: bubbles appear/disappear; settings persist
|
||||
- Restart app: bubble reappears at last saved position
|
||||
- Token-expired triggers tray balloon
|
||||
- Poll frequency change takes effect within one poll cycle
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Medium.** Most logic is structural — message routing and state management. Source repo has all the patterns.
|
||||
- Risk: dual-bubble on same UI thread with two HWNDs — should work, but verify message routing keys off `hwnd` parameter.
|
||||
- Risk: panel auto-close races with bubble re-click. Mitigation: 200 ms debounce on `WM_KILLFOCUS` before destroying panel; if bubble clicked within that window, cancel close.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Settings file written to `%APPDATA%`, user-scoped, no privileged ops.
|
||||
- Single-instance mutex name `Global\ClaudeCodeUsageBubble` — distinct from source app to allow coexistence.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 5: polish (HiDPI testing, multi-monitor, README, attribution, etc.)
|
||||
@@ -0,0 +1,120 @@
|
||||
# Phase 5: Polish & Finishing
|
||||
|
||||
## Context Links
|
||||
|
||||
- All prior phases
|
||||
- Source README for attribution / language list
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** Required before any release
|
||||
- **Status:** pending
|
||||
- **Description:** Multi-monitor and HiDPI verification, accessibility checks, README, license attribution, version 0.1.0 tag, optional CI workflow.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- Bubble renders crisply at 100% / 125% / 150% / 175% / 200% DPI
|
||||
- Bubble survives display add/remove (laptop dock/undock)
|
||||
- Bubble respects monitor work area when snapping (does not overlap taskbar)
|
||||
- README has install + run + uninstall sections
|
||||
- LICENSE has CodeZeno attribution paragraph
|
||||
- `--diagnose` flag works (writes `%TEMP%\claude-code-usage-bubble.log`)
|
||||
|
||||
### Non-functional
|
||||
- Cargo build is fully reproducible
|
||||
- No clippy warnings on `cargo clippy -- -D warnings` (or document the ones you keep)
|
||||
|
||||
## Architecture
|
||||
|
||||
No new modules.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**To modify:**
|
||||
- `README.md` — fill out final content
|
||||
- `LICENSE` — final attribution paragraph
|
||||
- Maybe `.github/workflows/build.yml` for CI Windows build
|
||||
|
||||
**To delete:** none.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **HiDPI manual test matrix:**
|
||||
- Windows 10 at 100% DPI: bubble visible, text readable, ring smooth
|
||||
- Windows 11 at 150%: same
|
||||
- 4K monitor at 200%: same
|
||||
- Mixed-DPI dual monitor: drag bubble between monitors → verify rescale on `WM_DPICHANGED`
|
||||
2. **Multi-monitor edge tests:**
|
||||
- Snap bubble to right edge of secondary monitor → settings saved with correct coords
|
||||
- Disconnect monitor → bubble should reposition to primary monitor's work area on next start
|
||||
- Test with taskbar on top / left / right (not just default bottom)
|
||||
3. **README content checklist:**
|
||||
- One-paragraph what-it-is + screenshot/gif placeholder
|
||||
- **Attribution section** (required by source MIT license):
|
||||
> This project is a derivative of [CodeZeno/Claude-Code-Usage-Monitor](https://github.com/CodeZeno/Claude-Code-Usage-Monitor) (MIT, © 2026 Code Zeno Pty Ltd). The usage-polling, updater, tray-icon, and localization modules are ported from that codebase with minor adaptations; the floating-bubble UI is original to this project.
|
||||
- Install: cargo build instructions; future winget block
|
||||
- Use: bubble + panel + tray icon described
|
||||
- Models: same content as source
|
||||
- Diagnostics: `--diagnose` flag, log path
|
||||
- Privacy: same content as source (credentials read locally, GitHub for updates)
|
||||
- License: MIT
|
||||
4. **LICENSE file** — include both:
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 <your name>
|
||||
|
||||
Portions of this software are derived from Claude Code Usage Monitor,
|
||||
Copyright (c) 2026 Code Zeno Pty Ltd, licensed under the MIT License.
|
||||
|
||||
<rest of MIT license text>
|
||||
```
|
||||
5. **Optional CI** (`.github/workflows/build.yml`):
|
||||
- Runs `cargo fmt --check`, `cargo clippy`, `cargo build --release` on `windows-latest`
|
||||
- Uploads artifact on tag
|
||||
6. **Smoke test before tagging:**
|
||||
- Run `claude-code-usage-bubble.exe`
|
||||
- Verify bubble appears with placeholder data (or real data if Claude CLI signed in)
|
||||
- Drag, snap, expand, menu, exit — all work
|
||||
- Re-launch → second instance exits silently
|
||||
- `claude-code-usage-bubble.exe --diagnose` → log file populated
|
||||
7. **Tag v0.1.0** (only after the above passes):
|
||||
- `git tag v0.1.0`
|
||||
- Push to GitHub
|
||||
- Create release with the .exe artifact attached (so `updater.rs` works for future versions)
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] HiDPI matrix tested
|
||||
- [ ] Multi-monitor edge tests done
|
||||
- [ ] README.md final
|
||||
- [ ] LICENSE attribution finalized
|
||||
- [ ] Diagnostic log verified
|
||||
- [ ] Clippy clean
|
||||
- [ ] CI workflow (optional)
|
||||
- [ ] Smoke test green
|
||||
- [ ] v0.1.0 tagged
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- App can be downloaded fresh, built once, and used end-to-end
|
||||
- Source repo attribution is unambiguous
|
||||
- No regressions vs phases 1-4
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Low.** Polish phase.
|
||||
- Possible regression: HiDPI bug discovered late — fix in `bubble.rs` painting code.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Verify `updater.rs` `current_install_channel()` still returns `Portable`. Re-enabling winget detection is a future task — not part of v0.1.0.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ v0.1.0 release; future tasks (out of scope for this port):
|
||||
- Winget package submission (when ready)
|
||||
- Custom bubble art per model (Claude orange, Codex green) replacing inherited icons
|
||||
- Optional bubble-size setting (S/M/L) in right-click menu
|
||||
- Auto-hide when fullscreen apps active
|
||||
@@ -0,0 +1,83 @@
|
||||
# Plan: claude-code-usage-bubble — port from CodeZeno/Claude-Code-Usage-Monitor
|
||||
|
||||
**Mode:** `/ck:xia --port`
|
||||
**Source repo:** `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor` (Rust ~5.8k LOC, MIT)
|
||||
**Target repo:** `/config/workspace/CodeZeno/claude-code-usage-bubble` (new)
|
||||
**Date:** 2026-05-15
|
||||
|
||||
## Source Manifest
|
||||
|
||||
- Path: `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor`
|
||||
- Branch: `main` @ `b5f038d` (v1.4.1)
|
||||
- License: MIT (attribution required in new repo README)
|
||||
- Scope: portable subsystems only — see `phase-02-port-portable-modules.md`
|
||||
|
||||
## Decision Matrix (Approved)
|
||||
|
||||
| Decision | Choice |
|
||||
|---|---|
|
||||
| Platform | Windows-only (Win32 GDI + layered window) |
|
||||
| WSL credential reading | Keep |
|
||||
| Snap-to-edge | On, 12px zone, monitor work area |
|
||||
| Click behavior | Left-click = toggle panel, right-click = menu |
|
||||
| Dual-model layout | Two independent bubbles, positions persisted per model |
|
||||
| Winget channel | Code kept, `current_install_channel` stubbed to Portable |
|
||||
| Single-instance mutex | `Global\ClaudeCodeUsageBubble` |
|
||||
| Auto-hide when fullscreen | **Yes** (added to phase 3) — detect via `SHQueryUserNotificationState` or `MonitorFromWindow + window-rect == monitor-rect` against foreground HWND |
|
||||
| Bubble size customization | **Yes**, free range 32–128 px persisted in settings.json; resize via Ctrl+MouseWheel on bubble (no S/M/L menu) |
|
||||
| Per-model bubble art | **No** — both models share the same bubble look; differentiation only via usage-percentage ring color |
|
||||
|
||||
## Dependency Matrix (Source → New)
|
||||
|
||||
| Source file | LOC | Action | Target file |
|
||||
|---|---|---|---|
|
||||
| `src/models.rs` | 19 | COPY | `src/models.rs` |
|
||||
| `src/diagnose.rs` | 52 | COPY | `src/diagnose.rs` |
|
||||
| `src/theme.rs` | 52 | COPY | `src/theme.rs` |
|
||||
| `src/poller.rs` | 1099 | COPY | `src/poller.rs` |
|
||||
| `src/updater.rs` | 510 | COPY + stub channel | `src/updater.rs` |
|
||||
| `src/tray_icon.rs` | 441 | COPY | `src/tray_icon.rs` |
|
||||
| `src/localization/*` | ~620 | COPY | `src/localization/*` |
|
||||
| `src/native_interop.rs` | 179 | ADAPT (drop taskbar/WinEvent helpers) | `src/native_interop.rs` |
|
||||
| `src/main.rs` | 40 | ADAPT (call `bubble::run` instead of `window::run`, rename single-instance mutex) | `src/main.rs` |
|
||||
| `src/window.rs` | 2847 | **REWRITE** as `bubble.rs` + `panel.rs` + `settings.rs` + `app.rs` | NEW |
|
||||
| `build.rs`, `Cargo.toml`, `src/icons/*` | — | ADAPT | NEW |
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Status | File |
|
||||
|---|---|---|---|
|
||||
| 1 | Bootstrap repo (Cargo.toml, build.rs, LICENSE, README, icons) | pending | `phase-01-bootstrap-repo.md` |
|
||||
| 2 | Port portable modules verbatim | pending | `phase-02-port-portable-modules.md` |
|
||||
| 3 | Build floating bubble window (layered alpha, GDI ring, drag-anywhere, snap) | pending | `phase-03-build-bubble-window.md` |
|
||||
| 4 | Build expanded panel + settings persistence + orchestration | pending | `phase-04-panel-and-orchestration.md` |
|
||||
| 5 | Polish: HiDPI, multi-monitor, startup registry, mutex, tray icon wiring, README | pending | `phase-05-polish-and-finishing.md` |
|
||||
|
||||
## Risk Score
|
||||
|
||||
**Medium.** Highest-risk surface is **phase 3** — circular layered window with HiDPI-aware GDI ring drawing. Source codebase has no precedent for that exact pattern; needs fresh implementation. All other phases are straightforward ports or thin orchestration.
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|---|---|---|
|
||||
| GDI ring + ClearType text on layered alpha window | High | Reference `window.rs` UpdateLayeredWindow + DIB section pattern (lines around layered painting); keep ring math simple (parametric arc) |
|
||||
| Drag + snap interaction on multi-monitor | Medium | Use `MonitorFromPoint` per move; clamp to nearest monitor work area |
|
||||
| Two-bubble position state | Low | Independent `BubbleState` structs in settings.json |
|
||||
| WSL credential read regressions | Low | Verbatim port; no behavioral changes |
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Phase 1: 1–2h
|
||||
- Phase 2: 1h (mostly file copies + import path fixes)
|
||||
- Phase 3: 6–10h (the heavy lift)
|
||||
- Phase 4: 3–5h
|
||||
- Phase 5: 2–4h
|
||||
|
||||
**Total:** ~15–22h of focused implementation.
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
The new repo is greenfield — rollback means `rm -rf /config/workspace/CodeZeno/claude-code-usage-bubble`. No source-repo changes; this plan does not modify the source app.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None. All three formerly-deferred items resolved by user on 2026-05-15 (see Decision Matrix rows 8–10).
|
||||
Reference in new issue
Block a user