mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-22 20:15:09 +00:00
refactor: rename the project to usage-monitor
Rename the crate, binary, product name, identifier, settings folder, log file, autostart entry, user agent, updater endpoint and release assets. Legacy Win32 clients still update: they fall back to the first .exe asset, and the legacy rollback workflow reads the package name from the ref it builds. Upgraded installs start with default settings.
This commit is contained in:
1 parent
88d39feeb1
commit
6afd603d5e
44 files changed
+224
-202
No files matched your search
@@ -67,7 +67,7 @@ jobs:
|
||||
run: |
|
||||
$match = Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"' | Select-Object -First 1
|
||||
$cargoVersion = $match.Matches.Groups[1].Value
|
||||
$info = (Get-Item target/release/claude-code-usage-bubble.exe).VersionInfo
|
||||
$info = (Get-Item target/release/usage-monitor.exe).VersionInfo
|
||||
$info | Format-List ProductName, CompanyName, FileDescription, FileVersion, ProductVersion, LegalCopyright
|
||||
if ($info.ProductVersion -ne $cargoVersion) {
|
||||
Write-Error "ProductVersion '$($info.ProductVersion)' does not match Cargo.toml version '$cargoVersion'"
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bubble-exe-${{ github.sha }}
|
||||
path: target/release/claude-code-usage-bubble.exe
|
||||
path: target/release/usage-monitor.exe
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
@@ -105,9 +105,17 @@ jobs:
|
||||
echo "::error::Cargo.toml version is '$actual' after patching, expected '$VERSION'."
|
||||
exit 1
|
||||
fi
|
||||
sed -i -E '/^name = "claude-code-usage-bubble"\r?$/{n;s/^version = "[^"]*"(\r?)$/version = "'"$VERSION"'"\1/}' Cargo.lock
|
||||
if ! tr -d '\r' < Cargo.lock | grep -A1 -x 'name = "claude-code-usage-bubble"' | grep -qx "version = \"$VERSION\""; then
|
||||
echo "::error::Cargo.lock has no claude-code-usage-bubble entry at version '$VERSION' after patching."
|
||||
# Refs from before the rename use the old package name, so read it
|
||||
# from the ref instead of naming it here.
|
||||
crate=$(tr -d '\r' < Cargo.toml | sed -nE 's/^name[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n 1)
|
||||
if [ -z "$crate" ]; then
|
||||
echo "::error::Cargo.toml has no package name."
|
||||
exit 1
|
||||
fi
|
||||
echo "CRATE=$crate" >> "$GITHUB_ENV"
|
||||
sed -i -E '/^name = "'"$crate"'"\r?$/{n;s/^version = "[^"]*"(\r?)$/version = "'"$VERSION"'"\1/}' Cargo.lock
|
||||
if ! tr -d '\r' < Cargo.lock | grep -A1 -x "name = \"$crate\"" | grep -qx "version = \"$VERSION\""; then
|
||||
echo "::error::Cargo.lock has no $crate entry at version '$VERSION' after patching."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -118,8 +126,13 @@ jobs:
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
# The release always carries the exe as `usage-monitor.exe`, whatever
|
||||
# the ref named it, and a copy keeps the bytes the digest covers.
|
||||
run: |
|
||||
exe=target/release/claude-code-usage-bubble.exe
|
||||
exe=target/release/usage-monitor.exe
|
||||
if [ "$CRATE" != usage-monitor ]; then
|
||||
cp "target/release/$CRATE.exe" "$exe"
|
||||
fi
|
||||
npm exec --prefix current --no -- tauri signer sign --app-version "$VERSION" "$exe"
|
||||
test -s "$exe.sig"
|
||||
|
||||
@@ -127,8 +140,8 @@ jobs:
|
||||
with:
|
||||
name: release-legacy-win-x64
|
||||
path: |
|
||||
target/release/claude-code-usage-bubble.exe
|
||||
target/release/claude-code-usage-bubble.exe.sig
|
||||
target/release/usage-monitor.exe
|
||||
target/release/usage-monitor.exe.sig
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: |
|
||||
exe=target/release/claude-code-usage-bubble.exe
|
||||
exe=target/release/usage-monitor.exe
|
||||
npx tauri signer sign --app-version "$VERSION" "$exe"
|
||||
test -s "$exe.sig"
|
||||
|
||||
@@ -128,8 +128,8 @@ jobs:
|
||||
with:
|
||||
name: release-win-x64
|
||||
path: |
|
||||
target/release/claude-code-usage-bubble.exe
|
||||
target/release/claude-code-usage-bubble.exe.sig
|
||||
target/release/usage-monitor.exe
|
||||
target/release/usage-monitor.exe.sig
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
}
|
||||
appimage=$(one target/release/bundle/appimage/*_amd64.AppImage)
|
||||
deb=$(one target/release/bundle/deb/*_amd64.deb)
|
||||
name="claude-code-usage-bubble_${VERSION}_amd64"
|
||||
name="usage-monitor_${VERSION}_amd64"
|
||||
mkdir -p dist
|
||||
cp "$appimage" "dist/$name.AppImage"
|
||||
cp "$appimage.sig" "dist/$name.AppImage.sig"
|
||||
|
||||
Generated
+1
-1
@@ -514,7 +514,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "claude-code-usage-bubble"
|
||||
name = "usage-monitor"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"auto-launch",
|
||||
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
[package]
|
||||
name = "claude-code-usage-bubble"
|
||||
name = "usage-monitor"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
description = "Floating bubble showing Claude Code and Codex usage"
|
||||
homepage = "https://github.com/tiennm99/claude-code-usage-bubble"
|
||||
repository = "https://github.com/tiennm99/claude-code-usage-bubble"
|
||||
homepage = "https://github.com/tiennm99/usage-monitor"
|
||||
repository = "https://github.com/tiennm99/usage-monitor"
|
||||
|
||||
[lib]
|
||||
name = "claude_code_usage_bubble_lib"
|
||||
name = "usage_monitor_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "claude-code-usage-bubble"
|
||||
name = "usage-monitor"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||

|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
# Claude Code Usage Bubble
|
||||
# Usage Monitor
|
||||
|
||||
A floating, draggable circular bubble that shows your Claude Code and/or
|
||||
Codex usage on Windows and Linux — inspired by the floating "memory boost
|
||||
@@ -87,9 +87,9 @@ and start the app again.
|
||||
|
||||
### Windows
|
||||
|
||||
Grab `claude-code-usage-bubble.exe` from the
|
||||
[Releases page](https://github.com/tiennm99/claude-code-usage-bubble/releases/latest).
|
||||
Put it anywhere on disk (e.g. `%LOCALAPPDATA%\ClaudeCodeUsageBubble\`) and
|
||||
Grab `usage-monitor.exe` from the
|
||||
[Releases page](https://github.com/tiennm99/usage-monitor/releases/latest).
|
||||
Put it anywhere on disk (e.g. `%LOCALAPPDATA%\UsageMonitor\`) and
|
||||
run it. The app self-updates from the same Releases feed.
|
||||
|
||||
**First-run note:** the binary is unsigned, so SmartScreen will show
|
||||
@@ -99,13 +99,13 @@ signing is on the roadmap.
|
||||
### Linux x86_64
|
||||
|
||||
From the same
|
||||
[Releases page](https://github.com/tiennm99/claude-code-usage-bubble/releases/latest),
|
||||
[Releases page](https://github.com/tiennm99/usage-monitor/releases/latest),
|
||||
pick one:
|
||||
|
||||
- `claude-code-usage-bubble_X.Y.Z_amd64.AppImage`: make it executable
|
||||
- `usage-monitor_X.Y.Z_amd64.AppImage`: make it executable
|
||||
(`chmod +x`) and run it. The AppImage self-updates in place.
|
||||
- `claude-code-usage-bubble_X.Y.Z_amd64.deb`: install it with
|
||||
`sudo apt install ./claude-code-usage-bubble_X.Y.Z_amd64.deb`. The app
|
||||
- `usage-monitor_X.Y.Z_amd64.deb`: install it with
|
||||
`sudo apt install ./usage-monitor_X.Y.Z_amd64.deb`. The app
|
||||
still checks for updates, but installing one is left to you: download the
|
||||
new `.deb` and install it the same way.
|
||||
|
||||
@@ -118,26 +118,26 @@ dependency, so `npm ci` installs it.
|
||||
On Windows:
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/tiennm99/claude-code-usage-bubble
|
||||
cd claude-code-usage-bubble
|
||||
git clone https://github.com/tiennm99/usage-monitor
|
||||
cd usage-monitor
|
||||
npm ci
|
||||
npm run tauri build -- --no-bundle
|
||||
```
|
||||
|
||||
The binary lands at `target\release\claude-code-usage-bubble.exe`.
|
||||
The binary lands at `target\release\usage-monitor.exe`.
|
||||
|
||||
On Linux x86_64, install the build dependencies first (Debian/Ubuntu):
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file \
|
||||
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
git clone https://github.com/tiennm99/claude-code-usage-bubble
|
||||
cd claude-code-usage-bubble
|
||||
git clone https://github.com/tiennm99/usage-monitor
|
||||
cd usage-monitor
|
||||
npm ci
|
||||
npm run tauri build -- --no-bundle
|
||||
```
|
||||
|
||||
The binary lands at `target/release/claude-code-usage-bubble`. To build the
|
||||
The binary lands at `target/release/usage-monitor`. To build the
|
||||
`.deb` and AppImage instead, also install `patchelf` (the AppImage needs it)
|
||||
and run `npm run tauri build -- --bundles deb,appimage`; they land under
|
||||
`target/release/bundle/`.
|
||||
@@ -194,34 +194,34 @@ same; these differences are visible:
|
||||
## Diagnostics
|
||||
|
||||
```text
|
||||
claude-code-usage-bubble.exe --diagnose # Windows
|
||||
claude-code-usage-bubble --diagnose # Linux .deb (pass the same flag to the AppImage)
|
||||
usage-monitor.exe --diagnose # Windows
|
||||
usage-monitor --diagnose # Linux .deb (pass the same flag to the AppImage)
|
||||
```
|
||||
|
||||
This writes a log file, truncated on each start, to:
|
||||
|
||||
| OS | Log file |
|
||||
|----|----------|
|
||||
| Windows | `%TEMP%\claude-code-usage-bubble.log` |
|
||||
| Linux | `$TMPDIR/claude-code-usage-bubble.log`, usually `/tmp/claude-code-usage-bubble.log` |
|
||||
| Windows | `%TEMP%\usage-monitor.log` |
|
||||
| Linux | `$TMPDIR/usage-monitor.log`, usually `/tmp/usage-monitor.log` |
|
||||
|
||||
Settings are saved to:
|
||||
|
||||
| OS | Settings file |
|
||||
|----|---------------|
|
||||
| Windows | `%APPDATA%\ClaudeCodeUsageBubble\settings.json` |
|
||||
| Linux | `$XDG_CONFIG_HOME/ClaudeCodeUsageBubble/settings.json`, usually `~/.config/ClaudeCodeUsageBubble/settings.json` |
|
||||
| Windows | `%APPDATA%\UsageMonitor\settings.json` |
|
||||
| Linux | `$XDG_CONFIG_HOME/UsageMonitor/settings.json`, usually `~/.config/UsageMonitor/settings.json` |
|
||||
|
||||
Other files the app writes:
|
||||
|
||||
| What | Windows | Linux |
|
||||
|------|---------|-------|
|
||||
| Update downloads and `update-error.log` | `%LOCALAPPDATA%\ClaudeCodeUsageBubble\updates\` | `~/.local/share/ClaudeCodeUsageBubble/updates/` |
|
||||
| Web view profile (cache, storage) | `%LOCALAPPDATA%\io.github.tiennm99.claudecodeusagebubble\` (WebView2) | `~/.local/share/io.github.tiennm99.claudecodeusagebubble/` (WebKitGTK) |
|
||||
| Update downloads and `update-error.log` | `%LOCALAPPDATA%\UsageMonitor\updates\` | `~/.local/share/UsageMonitor/updates/` |
|
||||
| Web view profile (cache, storage) | `%LOCALAPPDATA%\io.github.tiennm99.usagemonitor\` (WebView2) | `~/.local/share/io.github.tiennm99.usagemonitor/` (WebKitGTK) |
|
||||
|
||||
On Windows the app also writes its notification icon to
|
||||
`%LOCALAPPDATA%\ClaudeCodeUsageBubble\icon.png` and registers it under
|
||||
`HKCU\Software\Classes\AppUserModelId\io.github.tiennm99.claudecodeusagebubble`,
|
||||
`%LOCALAPPDATA%\UsageMonitor\icon.png` and registers it under
|
||||
`HKCU\Software\Classes\AppUserModelId\io.github.tiennm99.usagemonitor`,
|
||||
which Windows needs to show toasts from a portable exe.
|
||||
|
||||
## Privacy and security
|
||||
|
||||
@@ -18,5 +18,3 @@ order.
|
||||
release and deletes it afterwards.
|
||||
4. **Merge into `main`.** This reaches production: the next tag publishes the
|
||||
Tauri build to existing users through self-update.
|
||||
5. **Rename to `usage-monitor`.** Follow
|
||||
[the rename plan](plans/261021-1226-rename-to-usage-monitor/plan.md).
|
||||
+22
-14
@@ -67,11 +67,11 @@ A release carries exactly these files and nothing else:
|
||||
|
||||
| Asset | Built by | Read by |
|
||||
|-------|----------|---------|
|
||||
| `claude-code-usage-bubble.exe` | Windows job, `npm run tauri build -- --no-bundle` | Legacy Win32 clients (GitHub API) and Tauri Windows clients (`latest.json`) |
|
||||
| `claude-code-usage-bubble.exe.sig` | `tauri signer sign` on the exe | Tauri Windows clients |
|
||||
| `claude-code-usage-bubble_X.Y.Z_amd64.AppImage` | Linux job, `tauri bundle` | Tauri AppImage clients |
|
||||
| `claude-code-usage-bubble_X.Y.Z_amd64.AppImage.sig` | the bundler, with the signing key | Tauri AppImage clients |
|
||||
| `claude-code-usage-bubble_X.Y.Z_amd64.deb` | Linux job, `tauri bundle` | Users installing by hand (`.deb` installs check for updates but do not apply them) |
|
||||
| `usage-monitor.exe` | Windows job, `npm run tauri build -- --no-bundle` | Legacy Win32 clients (GitHub API) and Tauri Windows clients (`latest.json`) |
|
||||
| `usage-monitor.exe.sig` | `tauri signer sign` on the exe | Tauri Windows clients |
|
||||
| `usage-monitor_X.Y.Z_amd64.AppImage` | Linux job, `tauri bundle` | Tauri AppImage clients |
|
||||
| `usage-monitor_X.Y.Z_amd64.AppImage.sig` | the bundler, with the signing key | Tauri AppImage clients |
|
||||
| `usage-monitor_X.Y.Z_amd64.deb` | Linux job, `tauri bundle` | Users installing by hand (`.deb` installs check for updates but do not apply them) |
|
||||
| `latest.json` | `scripts/make-latest-json.js` in the publish job | Tauri clients, through the updater endpoint in `tauri.conf.json` |
|
||||
|
||||
`latest.json` holds `version`, `notes`, `pub_date` and exactly two
|
||||
@@ -82,11 +82,11 @@ Linux ARM64 or macOS entry.
|
||||
### Rename rule
|
||||
|
||||
The bundler names the Linux outputs after `productName`
|
||||
("Claude Code Usage Bubble"), and GitHub rewrites spaces in asset
|
||||
("Usage Monitor"), and GitHub rewrites spaces in asset
|
||||
names, which would break the `latest.json` URLs. The Linux job
|
||||
therefore copies the bundles to space-free kebab names before upload:
|
||||
`claude-code-usage-bubble_<version>_amd64.AppImage`, its `.sig`, and
|
||||
`claude-code-usage-bubble_<version>_amd64.deb`. The signature covers
|
||||
`usage-monitor_<version>_amd64.AppImage`, its `.sig`, and
|
||||
`usage-monitor_<version>_amd64.deb`. The signature covers
|
||||
the file content, not its name, so renaming keeps it valid. The
|
||||
bundler also signs the deb, but that signature is not published,
|
||||
because no updater entry uses it.
|
||||
@@ -97,13 +97,16 @@ Clients from v0.5.0 and earlier read `/releases/latest` through the
|
||||
GitHub API, which skips drafts and prereleases. Checked against their
|
||||
updater code at v0.4.0, v0.4.1 and v0.5.0, they need:
|
||||
|
||||
- A non-draft, non-prerelease release marked latest in this repo.
|
||||
- A non-draft, non-prerelease release marked latest in this repo. They
|
||||
query the repository's name from before the rename; GitHub answers that
|
||||
API request with a 301 to the current repository, which WinHTTP follows.
|
||||
- A `tag_name` of plain `vX.Y.Z`, higher than the running version
|
||||
(suffixes are ignored when comparing). The first Tauri release must
|
||||
therefore be above v0.5.0.
|
||||
- An asset named `claude-code-usage-bubble.exe` (matched
|
||||
case-insensitively), else the first asset ending in `.exe`. The
|
||||
release must carry exactly one `.exe` asset, under that name. The
|
||||
- An `.exe` asset. They look first for the exe name they were built
|
||||
under, which no release carries since the rename, then take the first
|
||||
asset ending in `.exe`. The release must therefore carry exactly one
|
||||
`.exe` asset, `usage-monitor.exe`. The
|
||||
`.exe.sig`, AppImage, deb and `latest.json` do not end in `.exe`.
|
||||
- The exe uploaded unmodified, so the `digest` (`sha256:<hex>`) that
|
||||
GitHub computes matches the downloaded body they hash. `tauri signer
|
||||
@@ -115,9 +118,14 @@ updater code at v0.4.0, v0.4.1 and v0.5.0, they need:
|
||||
deletes the `<exe>.old.<pid>`, `updates/update.exe` and
|
||||
`updates/updater-helper-*.exe` files they leave behind.
|
||||
|
||||
Since the rename, an upgraded install starts with default settings and
|
||||
keeps any autostart entry the legacy app registered: the settings folder
|
||||
and autostart entry names changed, and nothing migrates them. See
|
||||
[the rename plan](../plans/261021-1226-rename-to-usage-monitor/plan.md).
|
||||
|
||||
`scripts/make-latest-json.js` enforces the asset rules before
|
||||
`gh release create` runs: it fails on a missing platform entry or
|
||||
signature, on any `.exe` other than `claude-code-usage-bubble.exe`
|
||||
signature, on any `.exe` other than `usage-monitor.exe`
|
||||
(in any case), on any asset name with a space, and on any file outside
|
||||
the exact set above. Tauri clients read only `latest.json`, whose URLs
|
||||
must equal the asset names exactly.
|
||||
@@ -175,7 +183,7 @@ is available only after the Tauri port merges into `main`:
|
||||
dependency. It builds with `cargo build --release --locked`, signs
|
||||
the exe with the same updater key and writes a Windows-only
|
||||
`latest.json`. Check the draft:
|
||||
it holds `claude-code-usage-bubble.exe`, its `.sig` and `latest.json`.
|
||||
it holds `usage-monitor.exe`, its `.sig` and `latest.json`.
|
||||
4. Publish it: either edit the draft into a release marked latest, or
|
||||
delete the draft and re-run the workflow with `draft` set to `false`,
|
||||
which publishes it as latest.
|
||||
|
||||
Generated
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "claude-code-usage-bubble",
|
||||
"name": "usage-monitor",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-code-usage-bubble",
|
||||
"name": "usage-monitor",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "claude-code-usage-bubble",
|
||||
"name": "usage-monitor",
|
||||
"private": true,
|
||||
"description": "Floating bubble showing Claude Code and Codex usage",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -20,11 +20,11 @@ stay on disk until the phase that replaces each one deletes it.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Single root package (D1). `[lib] name = "claude_code_usage_bubble_lib"`,
|
||||
`path = "src/lib.rs"`; `[[bin]] name = "claude-code-usage-bubble"`.
|
||||
- Single root package (D1). `[lib] name = "usage_monitor_lib"`,
|
||||
`path = "src/lib.rs"`; `[[bin]] name = "usage-monitor"`.
|
||||
`src/main.rs` only holds
|
||||
`#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` and a call
|
||||
to `claude_code_usage_bubble_lib::run()`.
|
||||
to `usage_monitor_lib::run()`.
|
||||
- `tauri.conf.json` at the root: `build.frontendDist: "ui"`,
|
||||
`app.withGlobalTauri: true`, no `devUrl`, `app.windows: []`.
|
||||
- `productName`, `mainBinaryName` and `identifier` as in D16. No `version` key.
|
||||
@@ -89,7 +89,7 @@ stay on disk until the phase that replaces each one deletes it.
|
||||
`npm run lint`, `npm test`, `npm run tauri build -- --no-bundle`.
|
||||
- The Linux row also runs `npm run tauri build -- --bundles deb` (no
|
||||
patchelf needed), so a broken `bundle.icon` list fails before release.
|
||||
- The Windows row uploads `target/release/claude-code-usage-bubble.exe` with
|
||||
- The Windows row uploads `target/release/usage-monitor.exe` with
|
||||
`actions/upload-artifact` (name `bubble-exe-<sha>`, `retention-days: 7`).
|
||||
- Row `macos-latest`: toolchain, cache, `cargo check --all-targets --locked`
|
||||
only (the macOS compile gate; `ui/` must exist, nothing else is needed).
|
||||
|
||||
@@ -27,7 +27,7 @@ only two Win32 couplings: `src/net/winhttp.rs`, and the `CommandExt` import in
|
||||
- Timeouts: connect 60 s, receive 30 s, global 120 s.
|
||||
- gzip enabled.
|
||||
- TLS is rustls with the platform verifier, so corporate roots stay trusted.
|
||||
- User-Agent `claude-code-usage-bubble/<version>`.
|
||||
- User-Agent `usage-monitor/<version>`.
|
||||
- A per-request User-Agent replaces the agent's, as the Codex provider requires.
|
||||
- Proxy:
|
||||
- Env proxies: `net::env_proxy()` walks the variables ureq reads (`ALL_PROXY`, `HTTPS_PROXY`, `HTTP_PROXY`, either case) and keeps the first valid one, in ureq's order. ureq's `socks-proxy` feature is on, so a SOCKS entry such as Clash's `ALL_PROXY=socks5h://…` is used, not skipped. `NO_PROXY` applies to the chosen proxy.
|
||||
@@ -61,7 +61,7 @@ only two Win32 couplings: `src/net/winhttp.rs`, and the `CommandExt` import in
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] `src/net/mod.rs` on ureq: same `Client`/`RequestBuilder`/`Response` surface, `Transport(String)` replaces `Win`, `http_status_as_error(false)`, connect 60 s / receive 30 s / global 120 s, gzip, rustls with the platform verifier, `claude-code-usage-bubble/<version>` User-Agent with per-request override
|
||||
- [x] `src/net/mod.rs` on ureq: same `Client`/`RequestBuilder`/`Response` surface, `Transport(String)` replaces `Win`, `http_status_as_error(false)`, connect 60 s / receive 30 s / global 120 s, gzip, rustls with the platform verifier, `usage-monitor/<version>` User-Agent with per-request override
|
||||
- [x] Proxy: env proxies from ureq; `cfg(windows)` `platform::windows::system_proxy()` reads `ProxyEnable`/`ProxyServer`/`ProxyOverride` only when no env proxy is set; `core::proxy` parses per-scheme and bare servers, `;`-split override globs and `<local>`; `Client` holds a proxied and a direct agent and picks per request
|
||||
- [x] `refresh.rs`: Windows candidates and `CREATE_NO_WINDOW` behind `cfg(windows)`; Unix `claude -p .` / `codex exec .` searched in `PATH`, `~/.local/bin`, `~/.claude/local`, `/opt/homebrew/bin`, `/usr/local/bin`; child reaped on a detached thread
|
||||
- [x] `usage/mod.rs` restored (providers, registry, headers, refresh, `Error`, `UsageProvider`); `src/lib.rs` declares `creds`, `net` and `platform`
|
||||
|
||||
@@ -19,7 +19,7 @@ command and event bridge. All of it lives in the lib (D1); `run()` is the entry.
|
||||
|
||||
- Pre-Builder order inside `run()`:
|
||||
1. `--diagnose` sets up diag.
|
||||
2. `cfg(windows)`: WebView2 guard. If `GetAvailableCoreWebView2BrowserVersionString` fails or returns empty, `platform::windows::message_box_error` shows "Claude Code Usage Bubble needs the Microsoft Edge WebView2 Runtime" plus the Evergreen download URL, then the process exits 0.
|
||||
2. `cfg(windows)`: WebView2 guard. If `GetAvailableCoreWebView2BrowserVersionString` fails or returns empty, `platform::windows::message_box_error` shows "Usage Monitor needs the Microsoft Edge WebView2 Runtime" plus the Evergreen download URL, then the process exits 0.
|
||||
3. `--wait-pid <pid>` waits until that PID is gone, polling every 100 ms, capped at 15 s (D13). `OpenProcess`/`WaitForSingleObject` on Windows, `libc::kill(pid, 0)` on Unix. Log the wait and its outcome under `--diagnose`.
|
||||
4. Apply the Linux `GDK_BACKEND=x11` rule (D12).
|
||||
5. Register `tauri-plugin-single-instance` first, with a no-op callback.
|
||||
@@ -28,7 +28,7 @@ command and event bridge. All of it lives in the lib (D1); `run()` is the entry.
|
||||
- `setup()`:
|
||||
- Load settings, drop off-screen positions against `available_monitors()`, and read the theme.
|
||||
- Build `AppState` in a `Mutex`, plus a `ui_bridge` that holds the `AppHandle`. Provide `with_state(|s| …) -> T` returning owned data; it is the only way to read state before calling Tauri (D14).
|
||||
- Run the startup cleanup: delete `<exe filename>.old.*` siblings and, under `data_local_dir()/ClaudeCodeUsageBubble/updates/`, `update.exe` and `updater-helper-*.exe` (moved from `src/update/handoff.rs:83-108` and `src/update/install.rs:323-343` into `src/handoff.rs`, with a unit test on a temp dir). `update-error.log` is kept.
|
||||
- Run the startup cleanup: delete `<exe filename>.old.*` siblings and, under `data_local_dir()/UsageMonitor/updates/`, `update.exe` and `updater-helper-*.exe` (moved from `src/update/handoff.rs:83-108` and `src/update/install.rs:323-343` into `src/handoff.rs`, with a unit test on a temp dir). `update-error.log` is kept.
|
||||
- Start the scheduler thread and trigger the first poll.
|
||||
- `RunEvent::ExitRequested { code: None, api }` calls `api.prevent_exit()` (D18).
|
||||
- The scheduler thread receives messages over an `mpsc` channel with `recv_timeout`:
|
||||
|
||||
@@ -17,7 +17,7 @@ These replace the following Win32 pieces:
|
||||
- `src/tray/callback.rs`.
|
||||
- The `pixmap_to_hicon` block in `src/tray/badge.rs`.
|
||||
- The `TrackPopupMenu` menu (`src/app.rs:923-1193`), which calls `SetForegroundWindow(owner)` first (`src/app.rs:1151-1152`).
|
||||
- The Run-key autostart (`src/app.rs:1514-1527`, value name `ClaudeCodeUsageBubble` at `src/app.rs:51`).
|
||||
- The Run-key autostart (`src/app.rs:1514-1527`, value name `UsageMonitor` at `src/app.rs:51`).
|
||||
|
||||
The user approved `tauri-plugin-notification` with Windows AUMID registration
|
||||
for the alerts (D9).
|
||||
@@ -41,10 +41,10 @@ for the alerts (D9).
|
||||
- Linux: before building any tray, probe with `libloading` for the names the tray backend loads, in its order: `libayatana-appindicator3.so.1`, `libappindicator3.so.1`, then the unversioned `libayatana-appindicator3.so` and `libappindicator3.so` (its default `backcompat` feature; sometimes the only names in an AppImage). The loaded library stays loaded. If none loads, log it and skip tray creation, because the tray library panics on a missing library and the release profile aborts on panic. Any other tray creation error is logged and the app continues.
|
||||
- **Alerts** (D9):
|
||||
- Windows spike first: a fresh portable exe (CI artifact, no Start-menu shortcut, outside `target/`) on Windows 10 and 11 calls `platform::windows::register_aumid()` and shows one toast. If no toast appears, stop and report to the user before writing more alert code.
|
||||
- `register_aumid()` runs at startup before any toast: write `HKCU\Software\Classes\AppUserModelId\io.github.tiennm99.claudecodeusagebubble` with `DisplayName` "Claude Code Usage Bubble" and `IconUri` pointing at a PNG written to `data_local_dir()/ClaudeCodeUsageBubble/icon.png`, then `SetCurrentProcessExplicitAppUserModelID` with the same id.
|
||||
- `register_aumid()` runs at startup before any toast: write `HKCU\Software\Classes\AppUserModelId\io.github.tiennm99.usagemonitor` with `DisplayName` "Usage Monitor" and `IconUri` pointing at a PNG written to `data_local_dir()/UsageMonitor/icon.png`, then `SetCurrentProcessExplicitAppUserModelID` with the same id.
|
||||
- `tauri-plugin-notification` shows the alerts returned by `apply_results` and the pending "Update applied" alert from `--updated-to`. The update-applied alert ignores the cooldown.
|
||||
- **Autostart** (D10):
|
||||
- `tauri_plugin_autostart::Builder::new().app_name("ClaudeCodeUsageBubble")`, `MacosLauncher::LaunchAgent`, no args. Pin the Windows current-user mode if the Builder exposes it.
|
||||
- `tauri_plugin_autostart::Builder::new().app_name("UsageMonitor")`, `MacosLauncher::LaunchAgent`, no args. Pin the Windows current-user mode if the Builder exposes it.
|
||||
- On the pinned plugin version, read its Windows source and record in this file: what data `enable()` writes, whether `is_enabled()` compares the data, and whether `enable()` writes StartupApproved. If `is_enabled()` ignores the data, no migration code is written. Otherwise add the one-time data rewrite from D10 (legacy data exactly `"<current exe>"` and different from the plugin format), done through `platform::windows`, never touching StartupApproved.
|
||||
- Confirm that the Linux `.desktop` `Exec` uses `$APPIMAGE` when that variable is set. Otherwise, write the entry ourselves.
|
||||
- **Provider toggle:** keeps the at-least-one invariant, creates or destroys the bubble (Phase 06's `bubble::create`/`destroy`, stubbed until then; D18), syncs the tray, refreshes the menu and polls.
|
||||
@@ -69,8 +69,8 @@ for the alerts (D9).
|
||||
- [x] Headless menu test: the full tree (order, labels, ids, check and grey states) walked in `core::menu_model`
|
||||
- [x] `src/tray.rs`: one `TrayIconBuilder::with_id(slug)` per enabled live provider in `LIVE_USAGE` order, `show_menu_on_left_click(false)`, left `Click`/`Up` toggles the widget, badge always dark, tooltip from `tray_tooltip` cut to 127 UTF-16 units, `set_icon`/`set_tooltip` only on a changed `(pct, colour)` or text
|
||||
- [x] Linux: `platform::linux::appindicator_available()` probes `libayatana-appindicator3.so.1`, `libappindicator3.so.1`, `libayatana-appindicator3.so`, then `libappindicator3.so` (the backend's order); without any the tray is skipped and logged; other tray errors are logged
|
||||
- [x] Autostart plugin with `app_name("ClaudeCodeUsageBubble")`, no args, `MacosLauncher::LaunchAgent` on macOS (Linux and macOS); Windows pinned to a current-user entry with the quoted exe path through `auto-launch` directly (notes below); no migration code needed
|
||||
- [x] `platform::windows::register_aumid()` (HKCU `AppUserModelId` key with `DisplayName` and `IconUri`, PNG under `data_local_dir()/ClaudeCodeUsageBubble/icon.png`, `SetCurrentProcessExplicitAppUserModelID`) run in `setup()` before any toast
|
||||
- [x] Autostart plugin with `app_name("UsageMonitor")`, no args, `MacosLauncher::LaunchAgent` on macOS (Linux and macOS); Windows pinned to a current-user entry with the quoted exe path through `auto-launch` directly (notes below); no migration code needed
|
||||
- [x] `platform::windows::register_aumid()` (HKCU `AppUserModelId` key with `DisplayName` and `IconUri`, PNG under `data_local_dir()/UsageMonitor/icon.png`, `SetCurrentProcessExplicitAppUserModelID`) run in `setup()` before any toast
|
||||
- [x] `tauri-plugin-notification` shows the `apply_results` and session-expired alerts and the pending "Update applied" alert (no cooldown), after the first tray sync
|
||||
- [x] Provider toggle: at-least-one invariant, bubble window created or destroyed (stand-in window until the real bubble), tray sync, menu rebuild, poll
|
||||
- [x] `propagate()` now syncs the trays and rebuilds the menus when the tree changed
|
||||
@@ -86,7 +86,7 @@ for the alerts (D9).
|
||||
- Windows `is_enabled()` only checks that the `Run` value exists (in HKLM or HKCU) and is not disabled in `StartupApproved\Run`; it never compares the data. A legacy entry therefore shows checked and no data rewrite is written.
|
||||
- Windows `enable()` also writes the "enabled" bytes to `StartupApproved\Run` when that key exists. That only happens when the user ticks the item, which is the user re-enabling; our own code never touches `StartupApproved`, and `is_enabled()` honours a Task Manager disable.
|
||||
- The plugin Builder does not expose the Windows enable mode, so the plugin would use `auto-launch`'s default `Dynamic`: `enable()` tries HKLM first, so an elevated tick writes an all-users entry that a normal untick cannot remove (`disable()` ignores the HKLM access-denied error and `is_enabled()` still finds it), and that entry holds the unquoted path. Windows therefore does not register the plugin: `platform::windows::autostart_entry` builds an `auto_launch::AutoLaunch` with `WindowsEnableMode::CurrentUser` and the quoted exe path. Its data is `"<exe>" ` (the legacy quoted path plus the trailing space `auto-launch` always appends); `is_enabled()` is unchanged (value present in HKLM or HKCU, not disabled in `StartupApproved\Run`), so a legacy entry still shows checked. Linux and macOS keep the plugin (D10).
|
||||
- Linux `enable()` writes `~/.config/autostart/ClaudeCodeUsageBubble.desktop`, and the plugin uses `$APPIMAGE` (Tauri's `env().appimage`) as the path when it is set, so no entry of our own is needed. The `Exec` path is not quoted.
|
||||
- Linux `enable()` writes `~/.config/autostart/UsageMonitor.desktop`, and the plugin uses `$APPIMAGE` (Tauri's `env().appimage`) as the path when it is set, so no entry of our own is needed. The `Exec` path is not quoted.
|
||||
- `menu::refresh` is `Runtime::refresh_shell(force)`: on the main thread it snapshots state (lock released before any Tauri call), then syncs the trays and, when the menu tree differs from the last one applied (or after any menu click), gives every tray a freshly built menu. Countdown ticks and polls therefore rebuild no menu unless something in it changed. The snapshot is taken inside the main-thread closure because a closure queued from a worker runs after one a menu click runs inline; a snapshot taken on the calling thread could otherwise re-apply a stale tree and recreate a removed tray icon.
|
||||
- The left-click handler is registered once on the Tauri builder (`Builder::on_tray_icon_event`), not per icon: Tauri calls a per-icon handler while holding the per-icon listener table, and the inline main-thread refresh may build a missing icon, whose registration locks that same table (a same-thread deadlock).
|
||||
- Each tray gets its own `Menu` instance; the bubble right-click builds a fresh one and pops it from the main thread after the async command returns.
|
||||
@@ -105,7 +105,7 @@ for the alerts (D9).
|
||||
- Toggle a provider off and on from the tray menu, once slowly and once quickly: the app does not hang, and exactly one bubble and one tray icon come back.
|
||||
- Tick "Start with Windows" from an elevated run: only `HKCU\…\Run` gets the value, with the quoted path; a normal run can untick it.
|
||||
- The bubble right-click menu dismisses on a click elsewhere.
|
||||
- With a legacy `ClaudeCodeUsageBubble` Run value preset, the item shows checked, and there is exactly one entry after two launches. After a Task Manager disable, the item stays unchecked across two launches.
|
||||
- With a legacy `UsageMonitor` Run value preset, the item shows checked, and there is exactly one entry after two launches. After a Task Manager disable, the item stays unchecked across two launches.
|
||||
- Threshold, session-expired and update-applied toasts appear from a fresh portable exe with no shortcut.
|
||||
|
||||
## Risks
|
||||
|
||||
@@ -21,7 +21,7 @@ corners for the hit area (D4).
|
||||
|
||||
- **Window** (`src/bubble.rs`, rewritten):
|
||||
- Created with `WebviewWindowBuilder::new(app, "bubble-<slug>", WebviewUrl::App("bubble.html"))`.
|
||||
- Builder options: `transparent(true)`, `decorations(false)`, `shadow(false)`, `always_on_top(true)`, `skip_taskbar(true)`, `resizable(false)`, `maximizable(false)`, `minimizable(false)`, `focused(false)`, `focusable(false)`, `visible(false)`, `title("Claude Code Usage Bubble")`.
|
||||
- Builder options: `transparent(true)`, `decorations(false)`, `shadow(false)`, `always_on_top(true)`, `skip_taskbar(true)`, `resizable(false)`, `maximizable(false)`, `minimizable(false)`, `focused(false)`, `focusable(false)`, `visible(false)`, `title("Usage Monitor")`.
|
||||
- Size is `PhysicalSize(scale(w,dpi), scale(h_logical,dpi))`, recomputed on `ScaleFactorChanged`. Each size change re-sends the payload, whose `canvasW`, `canvasH` and `dpi` drive `layout.js` (D2).
|
||||
- Position is the saved position or `default_position` from the primary monitor's `work_area()`, then clamped.
|
||||
- Honour `widget_visible` when creating the window (intentional fix #7).
|
||||
|
||||
@@ -20,7 +20,7 @@ that window's class is in the ignore list, it is cloaked, or it is minimized.
|
||||
|
||||
- `platform::foreground_is_fullscreen() -> (bool, &'static str reason)`:
|
||||
- **Windows:** the existing evaluation, copied verbatim, with one change: the
|
||||
`ClaudeCodeUsageBubble` class check becomes "the foreground window belongs
|
||||
`UsageMonitor` class check becomes "the foreground window belongs
|
||||
to our own process" (`GetWindowThreadProcessId` compared with
|
||||
`std::process::id()`). The `Progman`, `WorkerW` and `Shell_TrayWnd` class
|
||||
checks stay. Uses `rect_covers_monitor` and the style bits from `core::geom`.
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies: [5]
|
||||
How v0.5.0 updates today:
|
||||
|
||||
1. It queries the GitHub API (`src/update/release.rs:48-80`) and takes the
|
||||
asset named exactly `claude-code-usage-bubble.exe` (`release.rs:7`, `:68`),
|
||||
asset named exactly `usage-monitor.exe` (`release.rs:7`, `:68`),
|
||||
or else the first asset ending in `*.exe`.
|
||||
2. It checks the asset's sha256 digest.
|
||||
3. It `self_replace`s its own exe, relaunches with `--wait-pid <pid>` and
|
||||
@@ -34,7 +34,7 @@ self-replaces on Windows and runs the plugin's install on the AppImage.
|
||||
same commit that adds `plugins.updater` to `tauri.conf.json`, because the
|
||||
plugin's config requires `pubkey` and plugin init fails without it:
|
||||
- `pubkey`: the content of the public key generated with `npx tauri signer generate`.
|
||||
- `endpoints`: `["https://github.com/tiennm99/claude-code-usage-bubble/releases/latest/download/latest.json"]`.
|
||||
- `endpoints`: `["https://github.com/tiennm99/usage-monitor/releases/latest/download/latest.json"]`.
|
||||
- `tauri.conf.json` keeps `bundle.createUpdaterArtifacts: false`; `release.yml`
|
||||
enables it through `--config` (Phase 10).
|
||||
- `src/update/mod.rs` keeps the status machine (`core::state::UpdateStatus`).
|
||||
@@ -54,7 +54,7 @@ self-replaces on Windows and runs the plugin's install on the AppImage.
|
||||
reach it. If the second check returns `None`, set Up to date. Then dispatch
|
||||
on the channel:
|
||||
- `Portable` (Windows): `ensure_writable` and `reject_unsafe_path` (kept),
|
||||
write the bytes to `data_local_dir()/ClaudeCodeUsageBubble/updates/update.exe`,
|
||||
write the bytes to `data_local_dir()/UsageMonitor/updates/update.exe`,
|
||||
call `self_replace`, delete the staged file, then
|
||||
`handoff::relaunch(&["--updated-to", v])`.
|
||||
- `AppImage` (Linux with `$APPIMAGE` set): `update.install(bytes)`, then
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies: [9]
|
||||
`.github/workflows/release.yml` builds one Windows exe on tag pushes, with a
|
||||
draft release on `workflow_dispatch`. `ci.yml` already exists from Phase 01.
|
||||
|
||||
v0.4.0 to v0.5.0 read `api.github.com/repos/tiennm99/claude-code-usage-bubble/releases/latest`,
|
||||
v0.4.0 to v0.5.0 read `api.github.com/repos/tiennm99/usage-monitor/releases/latest`,
|
||||
which skips drafts and prereleases, and draft assets are not downloadable
|
||||
without auth. No draft can exercise the migration, and the user decided against
|
||||
an end-to-end rehearsal and a throwaway repo. This phase instead checks
|
||||
@@ -40,7 +40,7 @@ host has no patchelf and no display.
|
||||
cover a build step that runs without it.
|
||||
- **win-x64** job (`windows-latest`):
|
||||
- `npm run tauri build -- --no-bundle`.
|
||||
- `npx tauri signer sign target/release/claude-code-usage-bubble.exe`. This
|
||||
- `npx tauri signer sign target/release/usage-monitor.exe`. This
|
||||
writes a separate `.sig` and leaves the exe bytes untouched, so the GitHub
|
||||
asset digest that old clients verify is the digest of the exe they run.
|
||||
- Upload the exe and its `.sig`.
|
||||
@@ -50,8 +50,8 @@ host has no patchelf and no display.
|
||||
`npm run tauri bundle -- --bundles appimage,deb --config …` with it.
|
||||
- Rename every output to space-free kebab names before upload, because the
|
||||
bundler names them after `productName` and GitHub rewrites spaces in asset
|
||||
names: `claude-code-usage-bubble_<ver>_amd64.AppImage`, the matching
|
||||
`.AppImage.sig`, and `claude-code-usage-bubble_<ver>_amd64.deb`. The
|
||||
names: `usage-monitor_<ver>_amd64.AppImage`, the matching
|
||||
`.AppImage.sig`, and `usage-monitor_<ver>_amd64.deb`. The
|
||||
signature covers the content, not the name.
|
||||
- **publish** job:
|
||||
- Download the artifacts.
|
||||
@@ -60,7 +60,7 @@ host has no patchelf and no display.
|
||||
`{version, notes, pub_date, platforms:{"windows-x86_64", "linux-x86_64"}}`,
|
||||
where each entry is a release-download URL plus the `.sig` content. It
|
||||
fails on any missing entry, on any `*.exe` asset other than
|
||||
`claude-code-usage-bubble.exe`, and on any asset name with a space (a
|
||||
`usage-monitor.exe`, and on any asset name with a space (a
|
||||
guard that runs after the rename).
|
||||
- Then `gh release create <tag> <assets…> latest.json --generate-notes`,
|
||||
adding `--draft` for a dispatch run. A tag run is non-draft,
|
||||
@@ -72,7 +72,7 @@ host has no patchelf and no display.
|
||||
`Cargo.toml` version, runs `cargo build --release --locked`, signs the exe
|
||||
with `tauri signer sign` (CLI from `npm ci --prefix current` on the default
|
||||
branch's lockfile, since the legacy ref has no `package.json`), runs `make-latest-json.js --windows-only`
|
||||
(from the current default branch) and publishes `claude-code-usage-bubble.exe`,
|
||||
(from the current default branch) and publishes `usage-monitor.exe`,
|
||||
its `.sig` and the Windows-only `latest.json`. The build job runs with
|
||||
`contents: read` and `persist-credentials: false` and uploads an artifact; a
|
||||
separate publish job holds `contents: write`, as in `release.yml`. Tauri Linux clients get
|
||||
@@ -93,10 +93,10 @@ host has no patchelf and no display.
|
||||
|
||||
| What the old client reads or needs | v0.4.0 | v0.4.1 | v0.5.0 | New release provides | Result |
|
||||
|------------------------------------|--------|--------|--------|----------------------|--------|
|
||||
| Endpoint: `GET /repos/tiennm99/claude-code-usage-bubble/releases/latest` (`release.rs:8-9`, `:49`) | same | same | same | The Tauri tag release is non-draft, non-prerelease and marked latest in the same repo | pass (code): re-read at all three tags (`release.rs:8-9`, `:49`); a tag run of `release.yml` publishes without `--draft`/`--prerelease`, with `--latest`, then fails if `gh api …/releases/latest` returns another tag. Draft check: pending |
|
||||
| Endpoint: `GET /repos/tiennm99/usage-monitor/releases/latest` (`release.rs:8-9`, `:49`) | same | same | same | The Tauri tag release is non-draft, non-prerelease and marked latest in the same repo | pass (code): re-read at all three tags (`release.rs:8-9`, `:49`); a tag run of `release.yml` publishes without `--draft`/`--prerelease`, with `--latest`, then fails if `gh api …/releases/latest` returns another tag. Draft check: pending |
|
||||
| Release JSON fields: `tag_name`, `assets[].name`, `assets[].browser_download_url`, optional `assets[].digest` (`release.rs:104-119`) | same | same | same | Standard GitHub release object; nothing from `latest.json` is read | pass (code): `release.rs:104-119` at all three tags; `gh release create` makes a standard release object |
|
||||
| Version gate: `tag_name` parsed with the suffix-ignoring compare, must be greater than the running version (`release.rs:60-63`) | same | same | same | Tag `vX.Y.Z` above `v0.5.0`, equal to `Cargo.toml` (release.yml tag check) | pass (code): `release.rs:60-63` at all three tags; the `prepare` job requires `vX.Y.Z[-suffix]` equal to `Cargo.toml`. The first Tauri tag must be above v0.5.0 (a version bump, not a workflow check) |
|
||||
| Asset pick: name equal, ASCII case-insensitive, to `claude-code-usage-bubble.exe`, else the first asset ending in `.exe` (`release.rs:7`, `:65-74`) | same | same | same | Exactly one `*.exe` asset, named `claude-code-usage-bubble.exe`; `.exe.sig`, `.AppImage`, `.AppImage.sig`, `.deb` and `latest.json` do not end in `.exe` (`make-latest-json.js` guard) | pass (code): `release.rs:7`, `:65-74` at all three tags; `make-latest-json.js` rejects any other `.exe` (any case) and any asset outside the exact set, and runs before `gh release create`. Draft check: pending |
|
||||
| Asset pick: name equal, ASCII case-insensitive, to `usage-monitor.exe`, else the first asset ending in `.exe` (`release.rs:7`, `:65-74`) | same | same | same | Exactly one `*.exe` asset, named `usage-monitor.exe`; `.exe.sig`, `.AppImage`, `.AppImage.sig`, `.deb` and `latest.json` do not end in `.exe` (`make-latest-json.js` guard) | pass (code): `release.rs:7`, `:65-74` at all three tags; `make-latest-json.js` rejects any other `.exe` (any case) and any asset outside the exact set, and runs before `gh release create`. Draft check: pending |
|
||||
| Checksum: SHA-256 of the downloaded body must equal `digest` `sha256:<hex>` when present (`release.rs:78`, `:86-100`; `install.rs` `download`) | `install.rs:79` | `install.rs:88` | `install.rs:88` | GitHub computes `digest` for every uploaded asset (v0.5.0 shows one); the exe is uploaded unmodified after signing, so the digest matches | pass (code): `release.rs:78`, `:86-98`; `install.rs:95` (v0.4.0) and `:104` (v0.4.1, v0.5.0) compare the body hash. `tauri signer sign` was run on this host with a throwaway key: it writes `<file>.sig` beside the binary and leaves the binary as built |
|
||||
| Download: 2xx after redirects from `browser_download_url`, whole body in memory | same | same | same | Same GitHub download URL form as today; the exe grows from about 0.75 MB to several MB, which the in-memory read handles | pass (code): `install.rs:79-108` (v0.4.0) and `:88-117` (v0.4.1, v0.5.0) read the whole body after a 2xx; the URL is GitHub's `browser_download_url` as today |
|
||||
| Swap and relaunch | Helper copy of the old exe runs `--apply-update` (`install.rs:44-45`, `:128`), waits for the parent, then starts the new exe with `--updated-to X` only (`install.rs:173`) | `self_replace` in-process, then `--wait-pid <pid> --updated-to X` (`install.rs:155-172`) | same as v0.4.1 (`install.rs:155-172`) | The Tauri exe accepts a bare start, `--updated-to X` alone, and `--wait-pid <pid> --updated-to X` (Phase 04 CLI). It never needs `--apply-update`. The v0.4.0 helper runs `run_cli` before any mutex (`main.rs:33`), and the Tauri lock is a different, per-session lock, so the helper cannot block the new instance | pass (code): v0.4.0 `install.rs:44-45`, `:128-143` (helper args), `:173` (`--updated-to` only), `main.rs:33` (`run_cli` before `app::run`; `main.rs` is identical at all three tags); v0.4.1 and v0.5.0 `install.rs:155-172`. The Tauri `src/cli.rs` `parse` accepts a bare start, `--updated-to X` and `--wait-pid <pid> --updated-to X` and ignores `--apply-update` (`unknown_flags_are_ignored`) |
|
||||
@@ -148,7 +148,7 @@ host has no patchelf and no display.
|
||||
## Implementation notes
|
||||
|
||||
- **Dry runs not run.** Both workflows refuse to build without the signing secret, and `release.yml` also without `plugins.updater.pubkey`, which is still empty (Phase 09 step 1 is a user action). Creating draft releases was also outside what this run may touch. The table therefore says "pass (code)"; the "Draft check" items stay open until the user runs step 3.
|
||||
- **Signing flow checked locally.** With a throwaway key (deleted afterwards) and the matching `pubkey` passed through `--config`, `npm run tauri build -- --bundles deb --config '{"bundle":{"createUpdaterArtifacts":true},…}' -- --locked` produced `Claude Code Usage Bubble_<ver>_<arch>.deb` plus a `.deb.sig`, and `tauri signer sign --app-version` wrote `<binary>.sig` beside an untouched binary. The bundler also signs the deb, so the rename step copies only the AppImage, its `.sig` and the deb; the deb signature is not published (no updater entry uses it). Bundle names carry spaces, as the phase expected.
|
||||
- **Signing flow checked locally.** With a throwaway key (deleted afterwards) and the matching `pubkey` passed through `--config`, `npm run tauri build -- --bundles deb --config '{"bundle":{"createUpdaterArtifacts":true},…}' -- --locked` produced `Usage Monitor_<ver>_<arch>.deb` plus a `.deb.sig`, and `tauri signer sign --app-version` wrote `<binary>.sig` beside an untouched binary. The bundler also signs the deb, so the rename step copies only the AppImage, its `.sig` and the deb; the deb signature is not published (no updater entry uses it). Bundle names carry spaces, as the phase expected.
|
||||
- **Runner args.** `npm run tauri build -- <opts> -- --locked` hands `--locked` to cargo (checked with an invalid flag, which cargo rejected), so release builds stay locked like the old `cargo build --release --locked`.
|
||||
- **`--app-version`.** Both signing steps bind the version into the signature's trusted comment, as `tauri build` does for the AppImage, so `requireSignedVersion` can be turned on later without re-signing anything.
|
||||
- **Legacy version patch.** `release-legacy.yml` patches the first `version =` line of `Cargo.toml`, then sets the same version on the package's own `Cargo.lock` entry with `sed`, so no dependency is re-resolved. Checked on this host against an archive of v0.5.0: `cargo metadata --locked` accepts the patched lockfile without rewriting it, and rejects it when the two versions differ. The release is created with `--target` set to the built commit, so the tag lands on the legacy sources, not the default branch.
|
||||
@@ -173,7 +173,7 @@ script, not against a real draft.
|
||||
|
||||
- `actionlint` and `yamllint` are clean on all workflows.
|
||||
- `npm test` covers `make-latest-json.js`, including the two-platform shape and the second-`*.exe` failure.
|
||||
- The dry-run draft contains exactly `claude-code-usage-bubble.exe`, its `.exe.sig`, one renamed `_amd64.AppImage` with its `.sig`, one renamed `_amd64.deb` and a `latest.json` with only `windows-x86_64` and `linux-x86_64`, whose URLs match the asset names exactly.
|
||||
- The dry-run draft contains exactly `usage-monitor.exe`, its `.exe.sig`, one renamed `_amd64.AppImage` with its `.sig`, one renamed `_amd64.deb` and a `latest.json` with only `windows-x86_64` and `linux-x86_64`, whose URLs match the asset names exactly.
|
||||
- The `release-legacy.yml` draft contains the exe, its `.sig` and a Windows-only `latest.json`.
|
||||
- Every row of the compatibility table reads pass, with the tag and `file:line` it was checked against, and the residual risk is written down.
|
||||
|
||||
@@ -184,7 +184,7 @@ script, not against a real draft.
|
||||
| The legacy-to-Tauri swap fails in the field (unproven end to end) | L x H | The static table above; WebView2 guard; `update-error.log`; `release-legacy.yml` rollback. Accepted by the user in place of a rehearsal. |
|
||||
| A missing secret silently produces an unsigned build | L x H | The signing step fails hard when the env var is empty. |
|
||||
| An old client picks the wrong asset | L x H | Exactly one `*.exe` asset, enforced by `make-latest-json.js`. |
|
||||
| The Tauri release is not the one `/releases/latest` returns | L x H | Tag runs publish non-draft, non-prerelease, `--latest`; check with `gh api repos/tiennm99/claude-code-usage-bubble/releases/latest -q .tag_name` after publishing. |
|
||||
| The Tauri release is not the one `/releases/latest` returns | L x H | Tag runs publish non-draft, non-prerelease, `--latest`; check with `gh api repos/tiennm99/usage-monitor/releases/latest -q .tag_name` after publishing. |
|
||||
| A draft dry run reaches users | L x H | `/releases/latest` skips drafts; the dry-run tag carries a suffix, which the push trigger and `prepare` refuse to publish; both drafts are deleted after inspection. |
|
||||
|
||||
## Rollback
|
||||
|
||||
@@ -31,7 +31,7 @@ parity checklist before the first Tauri release.
|
||||
- `docs/release-process.md`:
|
||||
- The version lives in `Cargo.toml` only (`tauri.conf.json` inherits it). Bump it, then run `cargo build` to refresh `Cargo.lock`.
|
||||
- Signing-key custody and secrets, with an offline backup.
|
||||
- The asset contract: the exact `claude-code-usage-bubble.exe`, with no other `*.exe`, plus `.sig` files and `latest.json`.
|
||||
- The asset contract: the exact `usage-monitor.exe`, with no other `*.exe`, plus `.sig` files and `latest.json`.
|
||||
- The dry-run steps, the asset rename rule, the legacy-client asset contract from the Phase 10 compatibility table, its residual risk (no end-to-end rehearsal), and the `release-legacy.yml` rollback procedure.
|
||||
- No plan IDs or phase numbers in code comments or commit messages.
|
||||
|
||||
@@ -51,7 +51,7 @@ parity checklist before the first Tauri release.
|
||||
- [ ] The Phase 10 legacy-client compatibility table has every row marked pass for v0.4.0, v0.4.1 and v0.5.0, and the residual risk is recorded.
|
||||
- [ ] A second launch exits silently. Restart relaunches (and the old process is gone). Exit ends the process.
|
||||
- [ ] Without the WebView2 runtime (Windows Sandbox or an LTSC VM), the exe shows the message box with the download link and exits.
|
||||
- [ ] `--diagnose` writes `%TEMP%\claude-code-usage-bubble.log`.
|
||||
- [ ] `--diagnose` writes `%TEMP%\usage-monitor.log`.
|
||||
|
||||
## Files
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Port the usage bubble to Tauri v2
|
||||
title: Port the usage monitor to Tauri v2
|
||||
description: >-
|
||||
Rebuild the Win32 bubble, panel, tray and updater on Tauri v2 with a plain
|
||||
HTML/CSS/SVG/JS UI, keeping Windows parity, running on Linux x86_64 and cfg-gating macOS.
|
||||
@@ -15,7 +15,7 @@ tags:
|
||||
created: '2026-10-21'
|
||||
---
|
||||
|
||||
# Port The Usage Bubble To Tauri v2
|
||||
# Port The Usage Monitor To Tauri v2
|
||||
|
||||
## Outcome
|
||||
|
||||
@@ -61,22 +61,22 @@ None of these is part of this plan.
|
||||
|
||||
| ID | Decision | Reason |
|
||||
|----|----------|--------|
|
||||
| D1 | **Layout: one package at the repo root, lib plus thin bin.** `Cargo.toml`, `build.rs`, `tauri.conf.json`, `.taurignore`, `capabilities/`, `icons/`, `src/`, `ui/`, `tests/js/` and `package.json` live at the root. There is no `src-tauri/`. `src/lib.rs` (`[lib] name = "claude_code_usage_bubble_lib"`) declares every module `pub` and exposes `run()`. `src/main.rs` only calls `run()` and carries `windows_subsystem`. Portable logic goes in `src/core/`, OS code in `src/platform/{windows,linux,macos}.rs`. JS tests live in `tests/js/`, so they are not embedded in the binary. `.taurignore` lists `ui/`, `tests/`, `scripts/`, `plans/`, `docs/` and `node_modules/`, so UI edits do not trigger a cargo rebuild. | Git history stays on `src/`. Unused `pub` items in a lib do not trip `dead_code`, so `clippy -D warnings` holds while later phases are still wiring consumers. The `_lib` suffix avoids the Windows pdb name clash between a lib and a bin of the same name (the Tauri template does the same). |
|
||||
| D1 | **Layout: one package at the repo root, lib plus thin bin.** `Cargo.toml`, `build.rs`, `tauri.conf.json`, `.taurignore`, `capabilities/`, `icons/`, `src/`, `ui/`, `tests/js/` and `package.json` live at the root. There is no `src-tauri/`. `src/lib.rs` (`[lib] name = "usage_monitor_lib"`) declares every module `pub` and exposes `run()`. `src/main.rs` only calls `run()` and carries `windows_subsystem`. Portable logic goes in `src/core/`, OS code in `src/platform/{windows,linux,macos}.rs`. JS tests live in `tests/js/`, so they are not embedded in the binary. `.taurignore` lists `ui/`, `tests/`, `scripts/`, `plans/`, `docs/` and `node_modules/`, so UI edits do not trigger a cargo rebuild. | Git history stays on `src/`. Unused `pub` items in a lib do not trip `dead_code`, so `clippy -D warnings` holds while later phases are still wiring consumers. The `_lib` suffix avoids the Windows pdb name clash between a lib and a bin of the same name (the Tauri template does the same). |
|
||||
| D2 | **Rendering:** the bubble is an HTML stadium pill (`border-radius: 9999px`). Head rings are SVG circles drawn with `stroke-dasharray`, tail bars are pill divs. Rust sets the window to the exact integer physical size and sends `canvasW`, `canvasH` (physical) and `dpi` in the payload; `ui/layout.js` works from those, never from `innerWidth * devicePixelRatio`. Stroke clamps (ring 2..4, time ring 2..3 physical px) carry over. Text widths come from canvas `measureText`. The bubble palette (Phase 06 table, `src/bubble.rs:1406-1427`, `1857-1866`) is CSS custom properties keyed by `[data-theme]`. Fill colours come from Rust `bar_fill_color` as hex. **Every displayed string is formatted in Rust** (half-to-even percent, countdowns, placeholders); JS never formats a number for display. The pulse is a JS 80 ms step with the exact per-element rules in Phase 06. | One owner per rule. Rust owns colours and strings; JS owns pixels. |
|
||||
| D3 | **Interaction:** pointerdown (button 0) calls `setPointerCapture` and records the screen point. Pointermove requires `(e.buttons & 1) === 1`; once the pointer has moved 3 or more physical px, JS invokes `bubble_drag_start`, clears its pressed state and calls `startDragging()`. Pressed state also clears on `pointercancel`, `lostpointercapture` and `blur`. A release before the threshold invokes `bubble_click`. On Windows, a `SetWindowSubclass` hook on the bubble HWND catches `WM_EXITSIZEMOVE` (move end), `WM_SETTINGCHANGE` (theme recheck and reclamp) and `WM_DISPLAYCHANGE` (reclamp). On Linux and macOS, move end is a 250 ms debounce on `WindowEvent::Moved` that is **armed only by `bubble_drag_start`**; on X11 the debounce also checks Button1 with x11rb `QueryPointer` and re-arms while it is still held. Snap disarms before its own `set_position`, so programmatic moves (snap, clamp, resize recentre, Reset position) never snap or persist. `contextmenu` invokes `show_context_menu`. Ctrl+wheel uses a non-passive `wheel` listener that invokes `bubble_resize_step`. | Matches the HTCAPTION move loop on Windows, avoids mid-drag snaps on X11 and keeps "no snap or save after resize or clamp". |
|
||||
| D4 | **Hit area (user-approved):** the transparent stadium corners become dead zones (CSS `pointer-events: none` outside the pill) instead of click-through. | WebView windows take input across their whole rectangle. `SetWindowRgn` loses the anti-aliased edge. The area involved is about 0.21·h² (roughly 920 px² at 66 px). |
|
||||
| D5 | **No activation (Windows):** bubbles are built with `focused(false)` and `focusable(false)`. `platform::windows::apply_no_activate` ORs `WS_EX_NOACTIVATE \| WS_EX_TOOLWINDOW` into bubble HWNDs; `apply_toolwindow` ORs only `WS_EX_TOOLWINDOW` into the panel HWND (it must take focus). Both run before the first show. On Windows, bubble show and hide both go through `platform::windows` (`SW_SHOWNOACTIVATE` / `SW_HIDE`), never mixed with Tauri `show()`/`hide()`. Bubbles are created hidden; JS invokes `bubble_ready` right after it applies the first `get_bubble_state` result to the DOM (no `requestAnimationFrame`), and Rust shows the bubble anyway 1.5 s after creation if `bubble_ready` has not arrived. | Keeps today's never-steals-focus behaviour, keeps the panel out of Alt-Tab, avoids a white flash, and cannot leave a bubble invisible because a hidden webview throttles rendering. |
|
||||
| D6 | **Data layer:** the `usage/` and `creds/` modules stay. `net/` keeps the same `Client`/`Response` surface, backed by ureq with `http_status_as_error(false)`, gzip, a 60 s connect timeout, a 30 s receive timeout, the same User-Agent and the platform verifier. A Windows system proxy is read from `HKCU\…\Internet Settings` with per-scheme parsing and `ProxyOverride` bypass (Phase 03); PAC/WPAD is dropped (intentional change). `refresh.rs` gates `CREATE_NO_WINDOW` and the `cmd.exe`/`powershell` candidates behind `cfg(windows)`; Unix uses the plain `claude` and `codex` commands. | Providers compile unchanged and 401/403 handling and header fallbacks keep working. |
|
||||
| D7 | **Settings: no migration.** The path stays `dirs::config_dir()/ClaudeCodeUsageBubble/settings.json`, and the schema, key names and physical-px positions are byte-compatible. No new keys. Tauri `app_config_dir` and the store plugin are not used. Monitor validation moves to a pure function fed by `available_monitors()`. | Existing users keep their position, size, providers and intervals without any conversion step. |
|
||||
| D7 | **Settings: no migration.** The path stays `dirs::config_dir()/UsageMonitor/settings.json`, and the schema, key names and physical-px positions are byte-compatible. No new keys. Tauri `app_config_dir` and the store plugin are not used. Monitor validation moves to a pure function fed by `available_monitors()`. | Existing users keep their position, size, providers and intervals without any conversion step. |
|
||||
| D8 | **Updater: hybrid on one key.** All platforms use `tauri-plugin-updater` for `check()` and `download()`, which verifies the minisign signature against `latest.json`. A custom `version_comparator` keeps the suffix-ignoring comparison. **Check** uses a builder with a 30 s timeout. **Apply** builds a fresh updater with a 10 min timeout, re-runs `check()` and calls `download()` on that result, because the builder timeout is a total-request timeout that also covers the download. On Windows both builders get the parsed system proxy. A `TargetNotFound` error from `check()` maps to Up to date. Windows stays a **portable exe**: the downloaded bytes go through `self_replace`. The Linux AppImage uses the plugin's `install()`. In both cases the app relaunches itself with `--wait-pid <pid> --updated-to X.Y.Z` and exits only if the spawn succeeded. A `.deb` install is a package-manager channel: the check runs, apply is greyed out. `tauri.conf.json` keeps `createUpdaterArtifacts: false`; only `release.yml` turns it on through `--config`, so local bundle builds need no key. The updater plugin is registered only from Phase 09, together with its `pubkey` and endpoint. No NSIS installer is published. | The portable model is part of Windows parity. Old clients pick the exact asset name through the GitHub API, so they swap straight into the Tauri exe. No released version ever ran the downloaded binary as `--apply-update` (see Backwards compatibility), so that handler is dropped. |
|
||||
| D9 | **Notifications (user-approved):** `tauri-plugin-notification` carries the threshold, session-expired and update-applied alerts. On Windows, before the first toast, `platform::windows::register_aumid()` writes `HKCU\Software\Classes\AppUserModelId\io.github.tiennm99.claudecodeusagebubble` with `DisplayName` "Claude Code Usage Bubble" and `IconUri` (a PNG written to `data_local_dir()/ClaudeCodeUsageBubble/icon.png`), then sets that AUMID for the process. A Windows spike at the start of the Phase 05 alerts work checks that a fresh portable exe with no Start-menu shortcut shows a toast on Windows 10 and 11; if it does not, stop and report to the user before writing more alert code. The 30-minute cooldown logic stays in Rust. | Tauri tray icons have no balloon API, and these alerts are part of Windows parity. Windows drops toasts for an unregistered AUMID. |
|
||||
| D10 | **Autostart:** `tauri-plugin-autostart` with `app_name("ClaudeCodeUsageBubble")`, the same Run value name as v0.5.0, so a legacy entry is already the plugin's entry. No migration code runs by default. Phase 05 verifies on the pinned plugin that `is_enabled()` does not compare the value data, that `enable()` writes HKCU (pin the current-user mode if the Builder exposes it) and what data `enable()` writes. Only if `is_enabled()` rejects the legacy `"<exe>"` data does a one-time rewrite happen: when the Run value data equals the legacy quoted path exactly and differs from the plugin format, overwrite the data directly in `platform::windows`. StartupApproved is never written by our code. The label is "Start with Windows" on Windows and "Start at login" elsewhere. | Avoids a double launch, a wrongly unchecked item, and silently undoing a Task Manager disable on every start. |
|
||||
| D9 | **Notifications (user-approved):** `tauri-plugin-notification` carries the threshold, session-expired and update-applied alerts. On Windows, before the first toast, `platform::windows::register_aumid()` writes `HKCU\Software\Classes\AppUserModelId\io.github.tiennm99.usagemonitor` with `DisplayName` "Usage Monitor" and `IconUri` (a PNG written to `data_local_dir()/UsageMonitor/icon.png`), then sets that AUMID for the process. A Windows spike at the start of the Phase 05 alerts work checks that a fresh portable exe with no Start-menu shortcut shows a toast on Windows 10 and 11; if it does not, stop and report to the user before writing more alert code. The 30-minute cooldown logic stays in Rust. | Tauri tray icons have no balloon API, and these alerts are part of Windows parity. Windows drops toasts for an unregistered AUMID. |
|
||||
| D10 | **Autostart:** `tauri-plugin-autostart` with `app_name("UsageMonitor")`, the same Run value name as v0.5.0, so a legacy entry is already the plugin's entry. No migration code runs by default. Phase 05 verifies on the pinned plugin that `is_enabled()` does not compare the value data, that `enable()` writes HKCU (pin the current-user mode if the Builder exposes it) and what data `enable()` writes. Only if `is_enabled()` rejects the legacy `"<exe>"` data does a one-time rewrite happen: when the Run value data equals the legacy quoted path exactly and differs from the plugin format, overwrite the data directly in `platform::windows`. StartupApproved is never written by our code. The label is "Start with Windows" on Windows and "Start at login" elsewhere. | Avoids a double launch, a wrongly unchecked item, and silently undoing a Task Manager disable on every start. |
|
||||
| D11 | **Fullscreen auto-hide:** Windows reuses `evaluate_foreground_fullscreen` verbatim, with the bubble-class check replaced by "foreground window owned by our PID". Linux X11/Xwayland uses `x11rb` with `_NET_ACTIVE_WINDOW` and `_NET_WM_STATE_FULLSCREEN`. Native Wayland and macOS return `false` (stubs). A 1500 ms thread drives all of them. | The proven logic is kept as-is. The PID check also covers the panel, the popup menu and WebView child windows. |
|
||||
| D12 | **Wayland:** when `WAYLAND_DISPLAY` is set and `GDK_BACKEND` is unset, set `GDK_BACKEND=x11` before Tauri starts. If no X server is present, positioning features become no-ops and a `(0,0)` position is never saved. | Positioning, snapping and topmost only work under Xwayland. |
|
||||
| D13 | **Single instance:** `tauri-plugin-single-instance` is registered first, and its callback does nothing. With `--wait-pid <pid>`, the pre-Builder wait loops until that PID is gone, capped at 15 s (today: 5 s wait plus about 3 s of mutex retries), and logs under `--diagnose`. `handoff::relaunch` returns `Result`; callers call `app.exit(0)` only on `Ok`. **Intentional change:** the lock is per user session instead of the `Global\` machine-wide mutex. | The plugin cannot retry, so the wait must outlast a slow WebView2 teardown. A failed spawn must not leave the user with no app. |
|
||||
| D14 | **Timers and threading:** one `std::thread` scheduler owns the poll deadline, the countdown deadline and the update-check deadline, fed by an `mpsc` channel with `recv_timeout`. The `AtomicBool` in-flight poll gate is kept. Polls run on worker threads. **Hard rule:** no Tauri window, tray, menu or emit API is called while the `AppState` mutex is held. Code takes an owned snapshot through `with_state(\|s\| …)`, drops the guard, then calls Tauri. `propagate`, `menu::refresh`, `tray::sync`, the fullscreen thread and every command follow it. | Plain std, no tokio dependency. Tauri tray, menu and window calls from a worker block on the main thread, so holding the lock across them deadlocks. |
|
||||
| D15 | **Logging:** keep `diag` (simplelog) behind the `--diagnose` gate, writing to `temp_dir()/claude-code-usage-bubble.log` and truncating on each start. It is initialised before the Builder. A `log_js` command forwards `window.onerror`. | The existing behaviour is kept with no new plugin. |
|
||||
| D16 | **Identity:** `productName` "Claude Code Usage Bubble", `mainBinaryName` "claude-code-usage-bubble", `identifier` "io.github.tiennm99.claudecodeusagebubble". `tauri.conf.json` has no `version`, so it inherits it from `Cargo.toml`. Release assets are renamed to space-free kebab names (Phase 10). | One version source for the CI tag check. The identifier is permanent, because it names the WebView data dir, the D-Bus single-instance name and the AUMID. |
|
||||
| D15 | **Logging:** keep `diag` (simplelog) behind the `--diagnose` gate, writing to `temp_dir()/usage-monitor.log` and truncating on each start. It is initialised before the Builder. A `log_js` command forwards `window.onerror`. | The existing behaviour is kept with no new plugin. |
|
||||
| D16 | **Identity:** `productName` "Usage Monitor", `mainBinaryName` "usage-monitor", `identifier` "io.github.tiennm99.usagemonitor". `tauri.conf.json` has no `version`, so it inherits it from `Cargo.toml`. Release assets are renamed to space-free kebab names (Phase 10). | One version source for the CI tag check. The identifier is permanent, because it names the WebView data dir, the D-Bus single-instance name and the AUMID. |
|
||||
| D17 | **CI:** `ci.yml` lands in **Phase 01** and runs on pushes to `feat/tauri-port` and `main` and on PRs. Rows: `windows-latest` and `ubuntu-22.04` run fmt, clippy, test, lint, `npm test` and `npm run tauri build -- --no-bundle`; the Windows row uploads the unsigned exe as an artifact (7-day retention), which is the input for every phase's Windows manual check. A `macos-latest` row runs only `cargo check --all-targets` and is the macOS compile gate. `release.yml` (Phase 10) is a matrix covering Windows x64 and Linux x64, plus a publish job. Only CI builds bundles (AppImage needs patchelf, which the runner installs). | Every phase can be verified on Windows without a Windows build host. |
|
||||
| D18 | **Window lifecycle:** bubbles are removed only with `destroy()`. A new window with the same label is created only after the old one's `Destroyed` event. Reset position moves the existing bubbles with `set_position` instead of recreating them. Bubbles get `prevent_close` on `CloseRequested`; the panel gets `prevent_close` plus `hide()`. `run()` handles `RunEvent::ExitRequested { code: None, api }` with `api.prevent_exit()`, so only an explicit `app.exit(0)` ends the process. | `close()` would be blocked by our own handler, re-using a label before destruction completes fails, and destroying the last window would otherwise quit the app. |
|
||||
|
||||
@@ -187,7 +187,7 @@ of that phase's last commit.
|
||||
platforms the armed Moved debounce does the same.
|
||||
5. Persistence: every change runs `Settings::save` (atomic write through tmp and
|
||||
rename). Other writes: the update staging file and `update-error.log` under
|
||||
`data_local_dir()/ClaudeCodeUsageBubble/updates/`, the AUMID icon PNG next to
|
||||
`data_local_dir()/UsageMonitor/updates/`, the AUMID icon PNG next to
|
||||
them and the HKCU AppUserModelId key.
|
||||
|
||||
## Backwards compatibility and migration
|
||||
@@ -249,7 +249,7 @@ exercised by real users of the first Tauri release (residual risk, Phase 10).
|
||||
- [ ] Every row of the Windows parity checklist (Phase 11) passes on Windows 10 and 11.
|
||||
- [ ] The Phase 10 legacy-client compatibility table is filled in from `git show <tag>:src/update/…` for v0.4.0, v0.4.1 and v0.5.0, every row passes against the dry-run asset list, and the residual risk is recorded.
|
||||
- [ ] `cargo fmt --check`, `cargo test`, `clippy -D warnings`, `npm run lint`, `npm test` and `npm run tauri build -- --no-bundle` are green on `windows-latest`, `ubuntu-22.04` and this host.
|
||||
- [ ] The `release.yml` dry-run draft holds exactly `claude-code-usage-bubble.exe`, its `.sig`, one x86_64 AppImage with `.sig`, one x86_64 `.deb` and a `latest.json` with only `windows-x86_64` and `linux-x86_64`.
|
||||
- [ ] The `release.yml` dry-run draft holds exactly `usage-monitor.exe`, its `.sig`, one x86_64 AppImage with `.sig`, one x86_64 `.deb` and a `latest.json` with only `windows-x86_64` and `linux-x86_64`.
|
||||
- [ ] The `macos-latest` `cargo check --all-targets` CI row is green.
|
||||
- [ ] The Linux x86_64 build runs on an X11 desktop, showing the bubble, data, menu, drag and snap.
|
||||
- [ ] No `windows` crate use exists outside `src/platform/windows.rs`.
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
---
|
||||
title: Rename to usage-monitor
|
||||
description: >-
|
||||
Rename the repository and product from claude-code-usage-bubble to
|
||||
usage-monitor once the Tauri port lands.
|
||||
status: pending
|
||||
Rename the repository and product to usage-monitor, with no trace of the
|
||||
previous name left in the repository.
|
||||
status: completed
|
||||
priority: P3
|
||||
branch: main
|
||||
branch: feat/tauri-port
|
||||
tags:
|
||||
- rename
|
||||
blockedBy:
|
||||
- 261021-1028-tauri-v2-port
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
created: '2026-10-21T05:26:00.000Z'
|
||||
---
|
||||
@@ -23,21 +22,28 @@ Both Claude Code and Codex call their quota "usage", and the name does not tie
|
||||
the app to the bubble shape or to one provider. This is a personal tool, so the
|
||||
user's preference decides the name.
|
||||
|
||||
## Constraints
|
||||
On 2026-10-22 the user chose to remove the previous name completely rather
|
||||
than keep it for compatibility, and to do the rename before the Tauri port
|
||||
merges into `main`.
|
||||
|
||||
- Installed legacy copies update only from an asset named
|
||||
`claude-code-usage-bubble.exe` in `tiennm99/claude-code-usage-bubble`
|
||||
releases. The first releases after the rename must keep publishing that
|
||||
asset name, and the old repository URL must keep redirecting.
|
||||
- Whether the legacy WinHTTP updater follows GitHub's API redirect after a
|
||||
repository rename is unverified. Verify it before relying on the redirect.
|
||||
## Result
|
||||
|
||||
## Steps
|
||||
- The GitHub repository is `tiennm99/usage-monitor`, and the local checkout
|
||||
moved to match. GitHub answers API requests for the old repository path
|
||||
with a 301, which the legacy WinHTTP client follows.
|
||||
- The crate, binary, `productName`, identifier, settings folder, log file,
|
||||
autostart entry, user agent, updater endpoint and release asset names all
|
||||
use the new name.
|
||||
- Legacy Win32 clients still find updates: they fall back to the first `.exe`
|
||||
asset when their own exe name is missing, so a release must carry exactly
|
||||
one `.exe`. `release-legacy.yml` reads the package name from the ref it
|
||||
builds and publishes the exe as `usage-monitor.exe`.
|
||||
|
||||
1. Rename the GitHub repository to `tiennm99/usage-monitor` with
|
||||
`gh repo rename`, then update `origin` and move the local checkout to
|
||||
`tiennm99/usage-monitor`.
|
||||
2. Update the updater's repository constant, `productName`, README, release
|
||||
workflow and docs to the new name. Keep publishing the legacy `.exe` asset
|
||||
name alongside the new one.
|
||||
3. Confirm a legacy copy still finds the update through the redirect.
|
||||
## Accepted costs
|
||||
|
||||
- Installs that upgrade start with default settings, because the settings
|
||||
folder name changed and nothing migrates it.
|
||||
- On Windows, an install that had autostart on keeps its old Run entry, which
|
||||
points at the same exe file. Turning autostart on in the new app adds a
|
||||
second entry.
|
||||
- Update leftovers in the old local data folder are no longer cleaned up.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Date:** 2026-05-16
|
||||
**Skill:** `/ck:brainstorm`
|
||||
**Goal:** Replace ~2,700 LOC of copied-from-upstream code in `claude-code-usage-bubble` with original implementations so attribution can be dropped honestly.
|
||||
**Goal:** Replace ~2,700 LOC of copied-from-upstream code in `usage-monitor` with original implementations so attribution can be dropped honestly.
|
||||
|
||||
---
|
||||
|
||||
@@ -153,7 +153,7 @@ Each TOML:
|
||||
|
||||
```toml
|
||||
[ui]
|
||||
window_title = "Claude Code Usage Bubble"
|
||||
window_title = "Usage Monitor"
|
||||
refresh = "Refresh"
|
||||
update_frequency = "Update frequency"
|
||||
# ...
|
||||
@@ -198,8 +198,8 @@ Source draws DIB sections + GDI primitives (rectangles, text). tiny-skia uses pa
|
||||
// Write %TEMP%\bubble-swap.bat:
|
||||
// @echo off
|
||||
// timeout /t 2 /nobreak >nul
|
||||
// move /y "%~dp0bubble-update.exe" "C:\Path\To\claude-code-usage-bubble.exe"
|
||||
// start "" "C:\Path\To\claude-code-usage-bubble.exe"
|
||||
// move /y "%~dp0bubble-update.exe" "C:\Path\To\usage-monitor.exe"
|
||||
// start "" "C:\Path\To\usage-monitor.exe"
|
||||
// del "%~f0"
|
||||
// Spawn cmd /c bubble-swap.bat with DETACHED_PROCESS | CREATE_NO_WINDOW
|
||||
// Exit current process
|
||||
@@ -226,7 +226,7 @@ Even simpler — no .bat file at all.
|
||||
// diag/mod.rs
|
||||
pub fn init(enabled: bool) -> Result<PathBuf> {
|
||||
if !enabled { return Ok(PathBuf::new()); }
|
||||
let path = std::env::temp_dir().join("claude-code-usage-bubble.log");
|
||||
let path = std::env::temp_dir().join("usage-monitor.log");
|
||||
WriteLogger::init(LevelFilter::Debug, Config::default(), File::create(&path)?)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Brainstorm: high-leverage improvements for claude-code-usage-bubble
|
||||
# Brainstorm: high-leverage improvements for usage-monitor
|
||||
|
||||
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).
|
||||
@@ -96,7 +96,7 @@ Only worthwhile if you want strangers using it. State the goal explicitly before
|
||||
|
||||
### 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.
|
||||
- Why: winget install tiennm99.UsageMonitor 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.
|
||||
|
||||
@@ -109,7 +109,7 @@ Only worthwhile if you want strangers using it. State the goal explicitly before
|
||||
### 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.
|
||||
- Crash dumps. The app is small and runs in a single Win32 message loop. simplelog to TEMP usage-monitor.log already covers 95 percent of post-mortem needs.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ User sees an occasional flash terminal window. Two paths today spawn `cmd.exe /c
|
||||
### New instance startup additions
|
||||
|
||||
```rust
|
||||
// Before acquiring Global\ClaudeCodeUsageBubble mutex:
|
||||
// Before acquiring Global\UsageMonitor mutex:
|
||||
if let Some(parent_pid) = parse_wait_pid_arg() {
|
||||
let h = OpenProcess(SYNCHRONIZE, FALSE, parent_pid)?;
|
||||
WaitForSingleObject(h, 5000); // 5s cap; proceed regardless
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Full-Project Code Review — claude-code-usage-bubble v0.1.2
|
||||
# Full-Project Code Review — usage-monitor 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.
|
||||
|
||||
@@ -69,7 +69,7 @@ Fix: validate `current_exe()` / `stage_path()` against `[A-Za-z0-9 \\:.\-_/]` be
|
||||
|
||||
`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.
|
||||
Fix: ship the release with a `usage-monitor.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`
|
||||
|
||||
@@ -50,7 +50,7 @@ plan.md "Intentionally changed behaviour" list:
|
||||
## 2. CI
|
||||
|
||||
Latest run on `feat/tauri-port` (HEAD `6407e49`, after this audit's doc fix):
|
||||
<https://github.com/tiennm99/claude-code-usage-bubble/actions/runs/39590034376>
|
||||
<https://github.com/tiennm99/usage-monitor/actions/runs/39590034376>
|
||||
is **success**. `windows-latest` ran fmt, clippy, tests, lint, `npm test`, the
|
||||
no-bundle build, the version-resource check and the exe artifact.
|
||||
`ubuntu-22.04` ran the same plus the deb bundle. `macos-latest` ran
|
||||
@@ -103,7 +103,7 @@ OK. `cargo test --locked` passed 216, with 1 ignored. `npm run lint` OK.
|
||||
- A Task Manager disable survives two launches.
|
||||
12. **Instances:** a second launch exits silently. Restart relaunches and the old process is gone. Exit ends the process.
|
||||
13. **No WebView2:** in Windows Sandbox or an LTSC VM, the exe shows the download-link message box and exits.
|
||||
14. **Diagnostics:** `--diagnose` writes `%TEMP%\claude-code-usage-bubble.log`.
|
||||
14. **Diagnostics:** `--diagnose` writes `%TEMP%\usage-monitor.log`.
|
||||
15. **Legacy update (after the first real release):** v0.4.0, v0.4.1 and v0.5.0 each self-update into the Tauri exe. Settings and autostart are kept, and the `.old.*` and `updater-helper-*` leftovers are cleaned up.
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Derivation audit: claude-code-usage-bubble vs Claude-Code-Usage-Monitor
|
||||
# Derivation audit: usage-monitor vs Claude-Code-Usage-Monitor
|
||||
|
||||
**Date:** 2026-05-16
|
||||
**Source repo:** `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor` — MIT, "Copyright (c) 2025 Craig Constable" per `LICENSE` (note: NOTICE in new repo claims "2026 Code Zeno Pty Ltd"; the actual upstream LICENSE names a person)
|
||||
**New repo:** `/config/workspace/tiennm99/claude-code-usage-bubble` — Apache-2.0 with NOTICE
|
||||
**New repo:** `/config/workspace/tiennm99/usage-monitor` — Apache-2.0 with NOTICE
|
||||
**Method:** byte comparison (`cmp -s`), `diff -u`, longest-contiguous-identical-run analysis (Python), line-set intersection (`comm -12`).
|
||||
|
||||
---
|
||||
@@ -24,14 +24,14 @@
|
||||
| `src/localization/korean.rs` | 46 | **COPY** | 100% | Byte-identical. |
|
||||
| `src/localization/spanish.rs` | 46 | **COPY** | 100% | Byte-identical. |
|
||||
| `src/localization/traditional_chinese.rs` | 46 | **COPY** | 100% | Byte-identical. |
|
||||
| `src/updater.rs` | 512 | **PATCH** | ~95% | 33-line diff. Changes: `RELEASE_ASSET_NAME`/`WINGET_PACKAGE_ID` renamed, `current_install_channel()` short-circuits to `Portable`, `ClaudeCodeUsageMonitor` -> `ClaudeCodeUsageBubble` in cache dir, three `#[allow(dead_code)]` attributes added. Algorithm and structure intact. |
|
||||
| `src/diagnose.rs` | 52 | **PATCH** | ~98% | Single-line rename: log file `claude-code-usage-monitor.log` -> `claude-code-usage-bubble.log`. Otherwise identical. |
|
||||
| `src/updater.rs` | 512 | **PATCH** | ~95% | 33-line diff. Changes: `RELEASE_ASSET_NAME`/`WINGET_PACKAGE_ID` renamed, `current_install_channel()` short-circuits to `Portable`, `ClaudeCodeUsageMonitor` -> `UsageMonitor` in cache dir, three `#[allow(dead_code)]` attributes added. Algorithm and structure intact. |
|
||||
| `src/diagnose.rs` | 52 | **PATCH** | ~98% | Single-line rename: log file `claude-code-usage-monitor.log` -> `usage-monitor.log`. Otherwise identical. |
|
||||
| `src/native_interop.rs` | 71 | **PATCH** | trimmed (108 LOC dropped) | Removed taskbar-embedding helpers (`find_taskbar`, `find_child_window`, `get_taskbar_rect`, `embed_in_taskbar`, WinEvent hooks). Kept `Color` struct, `colorref`, `wide_str`, `move_window`, `get_window_rect_safe`, timer/WM_APP constants — all verbatim names, signatures, bodies. Added two new timer/message consts (`TIMER_FULLSCREEN_CHECK`, `WM_APP_PANEL_TOGGLE`, `WM_APP_PANEL_CLOSE`). What remains is a verbatim subset of upstream. |
|
||||
| `src/main.rs` | 49 | **PATCH** | ~50% | Same shape (parse `--diagnose`, init logging, dispatch to UI). Module list expanded; `window::run()` -> `app::run()`; an `if let Ok(path) = ...` refactor replacing `match`. Modules `app`, `bubble`, `panel`, `settings`, `diag`, `net`, `os` added; `window` removed. |
|
||||
| `src/app.rs` | 1354 | **REWRITE** | ~25% line-set overlap with upstream `window.rs` | Carved out of upstream's 2847-LOC `window.rs`. Shares constants (`STARTUP_REGISTRY_PATH`, `STARTUP_VALUE_NAME`, `RETRY_BASE_MS`, `UPDATE_CHECK_INTERVAL_SECS`) and short adapted blocks (mutex setup, ~10 contiguous lines max). Structure genuinely reworked but lifts named consts, message-pump idioms, and update-scheduler logic. Fair-use derivative — *not* clean-room. |
|
||||
| `src/bubble.rs` | 817 | **REWRITE** | ~23% line-set overlap | New floating-bubble window (popup + layered alpha, hit-testing). Borrows Win32 boilerplate (`SendMessageW(WM_SETICON, ...)`, `ExtractIconExW`, `GetDpiForWindow`); longest contiguous identical run is 16 lines of icon-attach plumbing. Core bubble rendering (per-pixel alpha, circle hit-test) is original. |
|
||||
| `src/panel.rs` | 506 | **REWRITE** | ~24% line-set overlap | New popup panel for the expanded view. Longest identical run = 8 lines (generic GDI boilerplate). Original UI design (horizontal bars, session/weekly). |
|
||||
| `src/settings.rs` | 140 | **REWRITE** | ~26% line-set overlap (mostly `serde` boilerplate) | New `Settings` struct with `BubblePositions`, persisted to JSON in `%LOCALAPPDATA%\ClaudeCodeUsageBubble`. Mostly original; longest identical run = 4 lines. |
|
||||
| `src/settings.rs` | 140 | **REWRITE** | ~26% line-set overlap (mostly `serde` boilerplate) | New `Settings` struct with `BubblePositions`, persisted to JSON in `%LOCALAPPDATA%\UsageMonitor`. Mostly original; longest identical run = 4 lines. |
|
||||
| `src/diag/mod.rs` | 29 | **ORIGINAL** | 0% | New `log` + `simplelog` facade. No upstream counterpart. Parallels (not replaces) upstream's `diagnose.rs` which still exists in both. |
|
||||
| `src/os/mod.rs` | 14 | **ORIGINAL** | 0% | New. |
|
||||
| `src/os/string.rs` | 10 | **ORIGINAL** | 0% | New. |
|
||||
@@ -117,4 +117,4 @@ The plan's framing as a "clean-room rewrite" is technically inaccurate today: th
|
||||
3. Should `res/icon.rc`'s `CompanyName "tiennm99"` instead read something acknowledging the upstream icon copyright? The `.ico` is byte-identical to upstream's. The PE VERSIONINFO block is the public-facing metadata users see in Properties dialogs.
|
||||
|
||||
**Status:** DONE
|
||||
**Summary:** Forensic audit complete. ~34% of the new repo is byte-identical copy of upstream; another ~11% is trivially renamed; ~43% is structural derivative; only ~12% is fully original. NOTICE is legally required and has a copyright-name defect; can only be safely removed after the plan's Phase 6 retires the last COPY/PATCH files. Report written to `/config/workspace/tiennm99/claude-code-usage-bubble/plans/reports/derivation-audit-260516-0747.md`.
|
||||
**Summary:** Forensic audit complete. ~34% of the new repo is byte-identical copy of upstream; another ~11% is trivially renamed; ~43% is structural derivative; only ~12% is fully original. NOTICE is legally required and has a copyright-name defect; can only be safely removed after the plan's Phase 6 retires the last COPY/PATCH files. Report written to `/config/workspace/tiennm99/usage-monitor/plans/reports/derivation-audit-260516-0747.md`.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Best Practices Research: claude-code-usage-bubble
|
||||
# Best Practices Research: usage-monitor
|
||||
**Date:** 2026-05-16 | **Scope:** Current Rust desktop Windows ecosystem (2025/2026)
|
||||
|
||||
## 1. Self-Updating Rust Apps on Windows
|
||||
@@ -91,13 +91,13 @@ Current UX is acceptable for hobby indie app (<10MB binary). If you hit >5MB or
|
||||
## 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.
|
||||
- **winget** ([microsoft/winget-cli](https://github.com/microsoft/winget-cli)): Package manager for Windows; users run `winget install usage-monitor`. 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).
|
||||
Submit to **winget** (free, no certs needed for listing). Users get `winget install usage-monitor` + your app's auto-updater handles subsequent versions. Skip MSIX/MSI unless you need managed deployment (Intune/SCCM).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ Scope: Visual-only redesign of the stadium bubble (head ring + tail bars).
|
||||
Constraints: tiny-skia primitives only (AA fills, AA stroked arcs, AA pills) + GDI text. No new deps. No animations beyond existing pulse. Logical px values; `scale_to_dpi` handles HiDPI.
|
||||
|
||||
Files reviewed:
|
||||
- `D:\tiennm99\claude-code-usage-bubble\src\bubble.rs` (lines 1032–1703)
|
||||
- `D:\tiennm99\claude-code-usage-bubble\src\usage_color.rs`
|
||||
- `D:\tiennm99\claude-code-usage-bubble\src\os\color.rs`
|
||||
- `D:\tiennm99\usage-monitor\src\bubble.rs` (lines 1032–1703)
|
||||
- `D:\tiennm99\usage-monitor\src\usage_color.rs`
|
||||
- `D:\tiennm99\usage-monitor\src\os\color.rs`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
// because the same files are uploaded next to the manifest:
|
||||
//
|
||||
// - Legacy clients (v0.5.0 and earlier) read `/releases/latest` through the
|
||||
// GitHub API and download the asset named `claude-code-usage-bubble.exe`,
|
||||
// else the first asset ending in `.exe`. So exactly one `.exe` asset may
|
||||
// exist, under that exact name.
|
||||
// GitHub API and download the asset with the exe name they were built
|
||||
// under, else the first asset ending in `.exe`. No release carries their
|
||||
// old name, so exactly one `.exe` asset may exist.
|
||||
// - GitHub rewrites spaces in asset names, so a name with whitespace would no
|
||||
// longer match the URL written into the manifest.
|
||||
// - Every platform entry the app looks up must be present; a missing one
|
||||
@@ -19,8 +19,8 @@ import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
export const REPO = 'tiennm99/claude-code-usage-bubble';
|
||||
export const WINDOWS_EXE = 'claude-code-usage-bubble.exe';
|
||||
export const REPO = 'tiennm99/usage-monitor';
|
||||
export const WINDOWS_EXE = 'usage-monitor.exe';
|
||||
export const MANIFEST_NAME = 'latest.json';
|
||||
|
||||
const TAG_PATTERN = /^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
|
||||
@@ -63,7 +63,7 @@ export function platformAssets(version, windowsOnly) {
|
||||
/** @type {Record<string, string>} */
|
||||
const platforms = { 'windows-x86_64': WINDOWS_EXE };
|
||||
if (!windowsOnly) {
|
||||
platforms['linux-x86_64'] = `claude-code-usage-bubble_${version}_amd64.AppImage`;
|
||||
platforms['linux-x86_64'] = `usage-monitor_${version}_amd64.AppImage`;
|
||||
}
|
||||
return platforms;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function platformAssets(version, windowsOnly) {
|
||||
export function expectedAssets(version, windowsOnly) {
|
||||
const names = Object.values(platformAssets(version, windowsOnly)).flatMap((n) => [n, `${n}.sig`]);
|
||||
if (!windowsOnly) {
|
||||
names.push(`claude-code-usage-bubble_${version}_amd64.deb`);
|
||||
names.push(`usage-monitor_${version}_amd64.deb`);
|
||||
}
|
||||
return names.sort();
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ const READY_FALLBACK: Duration = Duration::from_millis(1500);
|
||||
#[cfg(not(windows))]
|
||||
const MOVE_END_DEBOUNCE: Duration = Duration::from_millis(250);
|
||||
|
||||
const TITLE: &str = "Claude Code Usage Bubble";
|
||||
const TITLE: &str = "Usage Monitor";
|
||||
|
||||
/// Work area used when no monitor can be enumerated.
|
||||
const FALLBACK_SCREEN: Rect = Rect::new(0, 0, 1920, 1080);
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ impl LocaleStrings {
|
||||
|
||||
fn english_strings() -> LocaleStrings {
|
||||
LocaleStrings {
|
||||
window_title: "Claude Code Usage Bubble".into(),
|
||||
window_title: "Usage Monitor".into(),
|
||||
refresh: "Refresh".into(),
|
||||
update_frequency: "Update frequency".into(),
|
||||
one_minute: "1 minute".into(),
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::core::geom::{
|
||||
};
|
||||
use crate::usage::ProviderId;
|
||||
|
||||
pub const APP_DIR_NAME: &str = "ClaudeCodeUsageBubble";
|
||||
pub const APP_DIR_NAME: &str = "UsageMonitor";
|
||||
const SETTINGS_FILE: &str = "settings.json";
|
||||
|
||||
pub const POLL_1_MIN: u32 = 60_000;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ use std::path::PathBuf;
|
||||
|
||||
use simplelog::{CombinedLogger, ConfigBuilder, LevelFilter, WriteLogger};
|
||||
|
||||
const LOG_FILE_NAME: &str = "claude-code-usage-bubble.log";
|
||||
const LOG_FILE_NAME: &str = "usage-monitor.log";
|
||||
|
||||
/// The updater plugin logs the proxy URL, credentials included, at debug
|
||||
/// level, so its messages are kept to info and above.
|
||||
|
||||
+7
-7
@@ -66,7 +66,7 @@ fn detach(command: &mut Command) {
|
||||
command.process_group(0);
|
||||
}
|
||||
|
||||
/// `data_local_dir()/ClaudeCodeUsageBubble/updates`, where updates are
|
||||
/// `data_local_dir()/UsageMonitor/updates`, where updates are
|
||||
/// staged and `update-error.log` lives.
|
||||
pub fn updates_dir() -> Option<PathBuf> {
|
||||
dirs::data_local_dir().map(|d| d.join(APP_DIR_NAME).join("updates"))
|
||||
@@ -153,14 +153,14 @@ mod tests {
|
||||
std::fs::create_dir_all(&install).expect("install dir");
|
||||
std::fs::create_dir_all(&updates).expect("updates dir");
|
||||
|
||||
let exe = install.join("claude-code-usage-bubble.exe");
|
||||
let exe = install.join("usage-monitor.exe");
|
||||
touch(&exe);
|
||||
touch(&install.join("claude-code-usage-bubble.exe.old.1234"));
|
||||
touch(&install.join("claude-code-usage-bubble.exe.old.99"));
|
||||
touch(&install.join("usage-monitor.exe.old.1234"));
|
||||
touch(&install.join("usage-monitor.exe.old.99"));
|
||||
touch(&install.join("other.exe.old.1"));
|
||||
touch(&install.join("settings.json"));
|
||||
// A directory with a matching name is left alone.
|
||||
std::fs::create_dir_all(install.join("claude-code-usage-bubble.exe.old.dir")).expect("dir");
|
||||
std::fs::create_dir_all(install.join("usage-monitor.exe.old.dir")).expect("dir");
|
||||
|
||||
touch(&updates.join("update.exe"));
|
||||
touch(&updates.join("updater-helper-4242.exe"));
|
||||
@@ -172,10 +172,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
names(&install),
|
||||
[
|
||||
"claude-code-usage-bubble.exe",
|
||||
"claude-code-usage-bubble.exe.old.dir",
|
||||
"other.exe.old.1",
|
||||
"settings.json",
|
||||
"usage-monitor.exe",
|
||||
"usage-monitor.exe.old.dir",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//! Claude Code Usage Bubble on Tauri v2.
|
||||
//! Usage Monitor on Tauri v2.
|
||||
//!
|
||||
//! The binary in `src/main.rs` only calls [`run`]. Every module is `pub` so
|
||||
//! that items wired up incrementally do not trip `dead_code`.
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
claude_code_usage_bubble_lib::run();
|
||||
usage_monitor_lib::run();
|
||||
}
|
||||
+2
-2
@@ -16,7 +16,7 @@ use ureq::{Agent, Proxy, ProxyProtocol};
|
||||
use crate::core::proxy::SystemProxy;
|
||||
|
||||
/// Default User-Agent; a per-request `User-Agent` header replaces it.
|
||||
pub const USER_AGENT: &str = concat!("claude-code-usage-bubble/", env!("CARGO_PKG_VERSION"));
|
||||
pub const USER_AGENT: &str = concat!("usage-monitor/", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
let request = received(&served).to_ascii_lowercase();
|
||||
let expected = format!("user-agent: {}\r\n", USER_AGENT.to_ascii_lowercase());
|
||||
assert!(request.contains(&expected), "{request}");
|
||||
assert!(USER_AGENT.starts_with("claude-code-usage-bubble/"));
|
||||
assert!(USER_AGENT.starts_with("usage-monitor/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -661,8 +661,8 @@ mod tests {
|
||||
#[test]
|
||||
fn run_value_path_is_quoted() {
|
||||
assert_eq!(
|
||||
quoted_path(Path::new(r"D:\My Tools\claude-code-usage-bubble.exe")),
|
||||
r#""D:\My Tools\claude-code-usage-bubble.exe""#
|
||||
quoted_path(Path::new(r"D:\My Tools\usage-monitor.exe")),
|
||||
r#""D:\My Tools\usage-monitor.exe""#
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-8
@@ -53,12 +53,12 @@ const REFRESH_TIMEOUT: Duration = Duration::from_secs(8);
|
||||
|
||||
/// Name of the start-at-login entry: the Windows `Run` value name of every
|
||||
/// earlier release, so an existing entry is already the plugin's entry.
|
||||
pub const AUTOSTART_NAME: &str = "ClaudeCodeUsageBubble";
|
||||
pub const AUTOSTART_NAME: &str = "UsageMonitor";
|
||||
|
||||
#[cfg(windows)]
|
||||
const WEBVIEW2_MISSING_TITLE: &str = "Claude Code Usage Bubble";
|
||||
const WEBVIEW2_MISSING_TITLE: &str = "Usage Monitor";
|
||||
#[cfg(windows)]
|
||||
const WEBVIEW2_MISSING_TEXT: &str = "Claude Code Usage Bubble needs the Microsoft Edge \
|
||||
const WEBVIEW2_MISSING_TEXT: &str = "Usage Monitor needs the Microsoft Edge \
|
||||
WebView2 Runtime.\n\nDownload the Evergreen runtime from:\n\
|
||||
https://developer.microsoft.com/microsoft-edge/webview2/";
|
||||
|
||||
@@ -905,7 +905,7 @@ pub fn run() {
|
||||
Ok(app) => app,
|
||||
Err(err) => {
|
||||
log::error!("tauri builder failed: {err}");
|
||||
eprintln!("Claude Code Usage Bubble failed to start: {err}");
|
||||
eprintln!("Usage Monitor failed to start: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
@@ -1015,10 +1015,7 @@ fn register_notification_identity(app: &AppHandle) {
|
||||
return;
|
||||
};
|
||||
let config = app.config();
|
||||
let name = config
|
||||
.product_name
|
||||
.as_deref()
|
||||
.unwrap_or("Claude Code Usage Bubble");
|
||||
let name = config.product_name.as_deref().unwrap_or("Usage Monitor");
|
||||
match platform::windows::register_aumid(&config.identifier, name, ICON_PNG, &icon_path) {
|
||||
Ok(()) => log::info!("notification app id {} registered", config.identifier),
|
||||
Err(err) => log::warn!("notification app id registration incomplete: {err}"),
|
||||
|
||||
+1
-1
@@ -439,7 +439,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
config.pointer("/plugins/updater/endpoints"),
|
||||
Some(&serde_json::json!([
|
||||
"https://github.com/tiennm99/claude-code-usage-bubble/releases/latest/download/latest.json"
|
||||
"https://github.com/tiennm99/usage-monitor/releases/latest/download/latest.json"
|
||||
]))
|
||||
);
|
||||
// The plugin parses this when it is registered; a parse error
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Claude Code Usage Bubble",
|
||||
"mainBinaryName": "claude-code-usage-bubble",
|
||||
"identifier": "io.github.tiennm99.claudecodeusagebubble",
|
||||
"productName": "Usage Monitor",
|
||||
"mainBinaryName": "usage-monitor",
|
||||
"identifier": "io.github.tiennm99.usagemonitor",
|
||||
"build": {
|
||||
"frontendDist": "ui"
|
||||
},
|
||||
@@ -35,7 +35,7 @@
|
||||
"updater": {
|
||||
"pubkey": "",
|
||||
"endpoints": [
|
||||
"https://github.com/tiennm99/claude-code-usage-bubble/releases/latest/download/latest.json"
|
||||
"https://github.com/tiennm99/usage-monitor/releases/latest/download/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
|
||||
const TAG = 'v0.6.0';
|
||||
const VERSION = '0.6.0';
|
||||
const APPIMAGE = `claude-code-usage-bubble_${VERSION}_amd64.AppImage`;
|
||||
const DEB = `claude-code-usage-bubble_${VERSION}_amd64.deb`;
|
||||
const APPIMAGE = `usage-monitor_${VERSION}_amd64.AppImage`;
|
||||
const DEB = `usage-monitor_${VERSION}_amd64.deb`;
|
||||
const NOW = new Date('2026-10-21T09:30:00.000Z');
|
||||
|
||||
/** @type {string} */
|
||||
@@ -42,7 +42,7 @@ const put = (names) => {
|
||||
}
|
||||
};
|
||||
|
||||
const FULL = ['claude-code-usage-bubble.exe', 'claude-code-usage-bubble.exe.sig', APPIMAGE, `${APPIMAGE}.sig`, DEB];
|
||||
const FULL = ['usage-monitor.exe', 'usage-monitor.exe.sig', APPIMAGE, `${APPIMAGE}.sig`, DEB];
|
||||
|
||||
describe('versionFromTag', () => {
|
||||
it('strips the v and keeps a pre-release suffix', () => {
|
||||
@@ -67,15 +67,15 @@ describe('run', () => {
|
||||
assert.equal(written.version, VERSION);
|
||||
assert.equal(written.pub_date, '2026-10-21T09:30:00.000Z');
|
||||
assert.deepEqual(Object.keys(written.platforms).sort(), ['linux-x86_64', 'windows-x86_64']);
|
||||
assert.equal(written.platforms['windows-x86_64'].signature, 'sig-of-claude-code-usage-bubble.exe.sig');
|
||||
assert.equal(written.platforms['windows-x86_64'].signature, 'sig-of-usage-monitor.exe.sig');
|
||||
assert.equal(written.platforms['linux-x86_64'].signature, `sig-of-${APPIMAGE}.sig`);
|
||||
});
|
||||
|
||||
it('points each URL at the renamed asset of the same name', () => {
|
||||
put(FULL);
|
||||
const { platforms } = run([TAG, dir], NOW);
|
||||
const base = 'https://github.com/tiennm99/claude-code-usage-bubble/releases/download/v0.6.0/';
|
||||
assert.equal(platforms['windows-x86_64'].url, `${base}claude-code-usage-bubble.exe`);
|
||||
const base = 'https://github.com/tiennm99/usage-monitor/releases/download/v0.6.0/';
|
||||
assert.equal(platforms['windows-x86_64'].url, `${base}usage-monitor.exe`);
|
||||
assert.equal(platforms['linux-x86_64'].url, `${base}${APPIMAGE}`);
|
||||
for (const { url } of Object.values(platforms)) {
|
||||
assert.ok(FULL.includes(url.slice(base.length)), url);
|
||||
@@ -83,7 +83,7 @@ describe('run', () => {
|
||||
});
|
||||
|
||||
it('writes only the Windows entry with --windows-only', () => {
|
||||
put(['claude-code-usage-bubble.exe', 'claude-code-usage-bubble.exe.sig']);
|
||||
put(['usage-monitor.exe', 'usage-monitor.exe.sig']);
|
||||
const manifest = run(['--windows-only', TAG, dir], NOW);
|
||||
assert.deepEqual(Object.keys(manifest.platforms), ['windows-x86_64']);
|
||||
});
|
||||
@@ -95,14 +95,14 @@ describe('run', () => {
|
||||
});
|
||||
|
||||
it('fails on an asset name with a space', () => {
|
||||
put([...FULL.filter((n) => n !== APPIMAGE), 'Claude Code Usage Bubble_0.6.0_amd64.AppImage']);
|
||||
put([...FULL.filter((n) => n !== APPIMAGE), 'Usage Monitor_0.6.0_amd64.AppImage']);
|
||||
assert.throws(() => run([TAG, dir], NOW), /whitespace/);
|
||||
});
|
||||
|
||||
it('fails on a second .exe asset, whatever its case', () => {
|
||||
put([...FULL, 'claude-code-usage-bubble-setup.exe']);
|
||||
put([...FULL, 'usage-monitor-setup.exe']);
|
||||
assert.throws(() => run([TAG, dir], NOW), /only \.exe asset allowed/);
|
||||
rmSync(join(dir, 'claude-code-usage-bubble-setup.exe'));
|
||||
rmSync(join(dir, 'usage-monitor-setup.exe'));
|
||||
put(['Other.EXE']);
|
||||
assert.throws(() => run([TAG, dir], NOW), /only \.exe asset allowed/);
|
||||
});
|
||||
@@ -113,13 +113,13 @@ describe('run', () => {
|
||||
});
|
||||
|
||||
it('fails when the AppImage carries another version', () => {
|
||||
put([...FULL.filter((n) => !n.startsWith(APPIMAGE)), 'claude-code-usage-bubble_0.5.0_amd64.AppImage']);
|
||||
put([...FULL.filter((n) => !n.startsWith(APPIMAGE)), 'usage-monitor_0.5.0_amd64.AppImage']);
|
||||
assert.throws(() => run([TAG, dir], NOW), /missing release assets/);
|
||||
});
|
||||
|
||||
it('fails on an empty signature', () => {
|
||||
put(FULL);
|
||||
writeFileSync(join(dir, 'claude-code-usage-bubble.exe.sig'), '\n');
|
||||
writeFileSync(join(dir, 'usage-monitor.exe.sig'), '\n');
|
||||
assert.throws(() => run([TAG, dir], NOW), /exe\.sig is empty/);
|
||||
});
|
||||
|
||||
@@ -146,8 +146,8 @@ describe('checkAssetNames', () => {
|
||||
describe('assetUrl', () => {
|
||||
it('builds the release-download URL', () => {
|
||||
assert.equal(
|
||||
assetUrl('v0.0.0-test', 'claude-code-usage-bubble.exe'),
|
||||
'https://github.com/tiennm99/claude-code-usage-bubble/releases/download/v0.0.0-test/claude-code-usage-bubble.exe',
|
||||
assetUrl('v0.0.0-test', 'usage-monitor.exe'),
|
||||
'https://github.com/tiennm99/usage-monitor/releases/download/v0.0.0-test/usage-monitor.exe',
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Claude Code Usage Bubble</title>
|
||||
<title>Usage Monitor</title>
|
||||
<link rel="stylesheet" href="bubble.css">
|
||||
<script type="module" src="bubble.js"></script>
|
||||
</head>
|
||||
|
||||
Reference in new issue
Block a user