Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Windows

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
package:
runs-on: windows-2022
timeout-minutes: 25
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- name: Test Windows contracts and portable calculations
run: pnpm test tests/windows tests/overview.test.ts tests/domain.test.ts
- name: Build unsigned NSIS and portable executables
run: pnpm package:win
- name: Smoke isolated packaged demo and single-instance relaunch
run: pnpm smoke:win
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: MeterUsage-Windows-x64-unsigned
path: release/*.exe
if-no-files-found: error
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ follows [Semantic Versioning](https://semver.org/).

- Optically center the Grok mark in provider rings, menu items, cards and
Settings. Preserve the original provider artwork and existing icon sizes.
- Preserve a finished provider command's result when Windows process cleanup
races its natural exit.

### Added

- Add a Windows x64 Electron source candidate with tray and side-notch usage,
local CLI activity, Codex browser sign-in through the installed CLI,
OpenRouter usage and one-time OpenAI or Anthropic API billing keys. Windows
desktop Claude, Antigravity and Grok Bot connections remain unavailable.
Windows CI passed focused tests, unsigned NSIS/portable packaging and an
isolated packaged-demo/single-instance smoke check. Interactive Windows
acceptance, installer acceptance and signing remain unverified. This is not
a Windows release.

## [0.3.0-beta.3] - 2026-10-17

Expand Down
18 changes: 13 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,23 @@ See [`docs/DEMO.md`](docs/DEMO.md) for the data and privacy rules.

## TypeScript Electron candidate

The source manifest retains 0.2.41. The public Electron beta is 0.3.0-beta.1,
build 54, with the version explicitly set during packaging. See the
The source manifest retains 0.2.41. The public Electron beta is 0.3.0-beta.3,
build 61, with the version explicitly set during packaging. See the
[beta guide](docs/BETA.md) for the signed artifact, known cosmetic footer issue
and incomplete native acceptance, and the [Electron guide](docs/electron/README.md)
for source choices and parity gaps.


Use the Node and pnpm versions declared in `package.json`. Provider accounts
are optional; synthetic tests and demo mode require no login. Shared checks
can run on Linux. Packaging, system symbol export and native acceptance require
macOS with the existing Swift and signing tools.
can run on Linux. macOS packaging, system symbol export and native acceptance
require macOS with the existing Swift and signing tools. The Windows source
candidate uses `pnpm package:win` to produce unsigned x64 NSIS and portable
executables. Its workflow passed typecheck, focused tests, packaging and an
isolated packaged-demo/single-instance smoke check at the revision recorded in
[ADR 0033](docs/adr/0033-windows-desktop-candidate.md#ci-validation).
Interactive Windows UI/provider checks, installer acceptance and signing
remain open.

`pnpm package:mac` also compiles the fixed Claude Desktop Keychain helper and
bundles it as an app resource. `pnpm build:desktop-helper` targets arm64 and
Expand Down Expand Up @@ -182,7 +188,9 @@ pnpm package:mac
packages an Apple silicon ZIP with publication disabled. It does not install
or replace an app. Locally packaged output is not the verified public beta
artifact. Keep the stable release and beta download routes separate.
Windows packaging, CI, native testing and release remain paused.
Windows packaging and a CI workflow are implemented in the candidate source.
They do not establish successful CI, native acceptance, signing or a release.
See the [platform pipeline](docs/plans/feature-and-windows-pipeline.md).

electron-builder resolves `pnpm` from PATH when collecting production
dependencies. If an existing launcher incorrectly loads a standalone pnpm
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ for rollback. Both apps share their identity and saved data, so do not run them
together against a normal profile. See [beta installation and limits](docs/BETA.md)
and the [version strategy](docs/VERSIONS.md).

Windows has an unsigned x64 source candidate, not a public download. The
candidate includes the tray, side notch, heatmaps, local CLI activity, Codex
browser sign-in through the installed CLI, OpenRouter and one-time OpenAI or
Anthropic API billing keys. Windows desktop Claude, Antigravity and Grok Bot
connections are unavailable. Native Windows acceptance, installer review and
signing remain open. See the [Windows candidate notes](docs/adr/0033-windows-desktop-candidate.md).

## Current beta UI

<p>
Expand Down
3 changes: 2 additions & 1 deletion Scripts/electron/after-pack.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ function renameExecutable(bundle, command = execFileSync) {
command("/usr/libexec/PlistBuddy", ["-c", "Set :CFBundleExecutable meterusage", join(bundle, "Contents/Info.plist")]);
}
module.exports = async context => {
if (context.electronPlatformName !== "darwin") throw new Error("Only macOS candidate packaging is enabled");
if (context.electronPlatformName === "win32") return;
if (context.electronPlatformName !== "darwin") throw new Error("Unsupported packaging platform");
renameExecutable(join(context.appOutDir, "MeterUsage.app"));
};
module.exports.renameExecutable = renameExecutable;
Binary file added Scripts/electron/assets/copilot-symbol.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Scripts/electron/assets/cursor-symbol.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Scripts/electron/assets/gemini-symbol.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Scripts/electron/assets/openRouter-symbol.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions Scripts/electron/export-portable-assets.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { copyFileSync, mkdirSync, readFileSync, writeFileSync } = require("node:fs");
const { join, resolve } = require("node:path");

function exportAssets(root) {
const resources = join(root, "Resources");
for (const provider of ["openRouter", "cursor", "copilot", "gemini"]) {
copyFileSync(join(__dirname, "assets", `${provider}-symbol.png`), join(resources, `${provider}-symbol.png`));
}
// Reuse the original icon's 256px PNG without image conversion tools.
const icns = readFileSync(join(resources, "AppIcon.icns"));
if (icns.toString("ascii", 0, 4) !== "icns" || icns.readUInt32BE(4) !== icns.length) throw new Error("Invalid app icon");
let png;
for (let offset = 8; offset < icns.length;) {
const size = icns.readUInt32BE(offset + 4);
if (size < 8 || offset + size > icns.length) throw new Error("Invalid app icon entry");
if (icns.toString("ascii", offset, offset + 4) === "ic08") png = icns.subarray(offset + 8, offset + size);
offset += size;
}
if (!png || png.readUInt32BE(16) !== 256 || png.readUInt32BE(20) !== 256) throw new Error("Missing 256px app icon");
const header = Buffer.alloc(22);
header.writeUInt16LE(1, 2); header.writeUInt16LE(1, 4);
header.writeUInt16LE(1, 10); header.writeUInt16LE(32, 12);
header.writeUInt32LE(png.length, 14); header.writeUInt32LE(22, 18);
mkdirSync(join(root, "build"), { recursive: true });
writeFileSync(join(root, "build", "AppIcon.ico"), Buffer.concat([header, png]));
}

if (require.main === module) exportAssets(resolve(__dirname, "../.."));
module.exports = { exportAssets };
40 changes: 40 additions & 0 deletions Scripts/electron/smoke-windows.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const { spawn, spawnSync } = require("node:child_process");
const { existsSync, mkdtempSync, realpathSync, rmSync } = require("node:fs");
const { tmpdir } = require("node:os");
const { dirname, join, resolve } = require("node:path");
const { setTimeout: delay } = require("node:timers/promises");

async function smoke(executable) {
if (process.platform !== "win32") throw new Error("Windows smoke requires Windows");
if (existsSync(join(dirname(executable), "resources", "desktop-keychain"))) throw new Error("Windows package includes the macOS Keychain helper");
const root = mkdtempSync(join(realpathSync(tmpdir()), "meterusage-windows-smoke-"));
const args = ["--demo", "--candidate-profile", join(root, "profile")];
const env = { ...process.env };
delete env.ELECTRON_RUN_AS_NODE;
let first, second;
const alive = child => child.pid && child.exitCode === null && child.signalCode === null;
const launch = () => {
const child = spawn(executable, args, { env, stdio: "ignore" });
child.on("error", () => {});
return child;
};
try {
first = launch();
await delay(10000);
if (!alive(first) || !existsSync(join(root, "profile", "electron"))) throw new Error("Packaged demo did not remain running in its isolated profile");
second = launch();
for (let attempt = 0; attempt < 50 && alive(second); attempt++) await delay(100);
if (second.exitCode !== 0 || !alive(first)) throw new Error("Packaged single-instance relaunch failed");
console.log("Windows packaged demo stayed alive; second invocation exited successfully.");
} finally {
for (const child of [second, first]) {
if (child && alive(child)) {
const result = spawnSync("taskkill.exe", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
if (result.status !== 0) throw new Error("Could not stop packaged smoke process tree");
}
}
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
}
}

if (require.main === module) smoke(resolve(process.argv[2] || "release/win-unpacked/MeterUsage.exe")).catch(error => { console.error(error.message); process.exitCode = 1; });
5 changes: 4 additions & 1 deletion docs/BETA.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ See [source choices](electron/README.md#source-choices),
- Live Codex refresh was observed. Claude paid-account allowance was not
live-tested. Optional support-report delivery and deployed relay limits
were not verified end to end.
- Windows support is paused. Intel Mac binaries are not provided by this beta.
- This beta remains macOS-only. A separate Windows x64 source candidate exists,
with native and installer acceptance, signing and public
release still open. See [ADR 0033](adr/0033-windows-desktop-candidate.md).
Intel Mac binaries are not provided by this beta.

Use Settings > Diagnostics for sanitized diagnostics and **Send report**.
The app limits report attempts to one every ten minutes, including relaunches.
Expand Down
22 changes: 18 additions & 4 deletions docs/KB.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Project knowledge

Release documentation refresh: 2026-10-17; build 61 executable inputs are unchanged.
Documentation refresh: 2026-10-18; the public Mac build 61 executable inputs are unchanged.

## Edition documentation

Expand Down Expand Up @@ -188,7 +188,20 @@ Release documentation refresh: 2026-10-17; build 61 executable inputs are unchan
- Side notch chrome is derived per theme in `SideNotchPanelView` by an HSV mix pinned to the popover surface's Rec. 709 relative luminance (`blendTinted`), so every accent renders at the same perceived brightness as the window beside it. The notch bands and hues delegate to the shared `headroomColor`/`MU` scale (80/95 thresholds), and the panel window forces `darkAqua` so those tokens resolve their dark variants on the strip.
- ADR 0006 replaces fixed alternate cases with a managed account list for Codex and Claude. Generated app IDs, provider, user label, directory and enabled state persist as JSON in UserDefaults Data. Primary keys retain their old form; additional history/archive keys use provider plus generated ID, and schema-1 reports add `account` labels. Rename preserves identity; remove stops metering without deleting provider files. No provider account identity is read for labels.
- ADR 0010 selects macOS TypeScript/Electron/React with pnpm, Vite+, Tailwind, Vitest and electron-builder. Main owns provider files/keys, native actions and persistence; a separate sandbox-compatible preload exposes sender-validated IPC to React. The candidate keeps JSON/history/archive and known preference formats. Swift remains the oracle and rollback path. No server or remote control layer is added.
- Windows implementation, installers, CI, testing and releases are paused until a test machine is available. The former RN/Tauri Windows-first plan is superseded; shared portable source does not establish Windows support.
- A Windows x64 Electron source candidate now covers tray and side-notch usage,
local CLI activity, Codex browser sign-in through the installed CLI,
OpenRouter and one-time OpenAI or Anthropic API billing keys. Windows desktop
Claude, Antigravity and Grok Bot account connections are unavailable. The
candidate stores preferences, history and sanitized billing snapshots under
`%APPDATA%\MeterUsage` (normally the user's `AppData\Roaming\MeterUsage`);
Windows does not read the macOS defaults or Keychain stores, and one-time API
keys are memory-only. `package:win` configures unsigned x64 NSIS and portable
executables. Windows CI passed 78 focused tests, packaging and the isolated
packaged-demo/single-instance smoke check for `82b7ca2`; see the
[CI receipt](adr/0033-windows-desktop-candidate.md#ci-validation).
Interactive GUI/provider behavior, installer acceptance and signing remain
unverified. No Windows release is available. The former RN/Tauri
Windows-first plan is superseded. See [ADR 0033](adr/0033-windows-desktop-candidate.md).
- Candidate checks use synthetic fixtures. Demo/candidate selection isolates preferences, history, caches and Electron state before composition. Live provider/key discovery, updater installation and login-item changes are disabled in demo. `pnpm package:mac` creates a separate candidate with publication disabled, using existing macOS tools.
- Candidate account visibility rechecks directory presence. Sharing freezes the displayed account key. Pacing-off hides forecasts; notch details reuse window-scoped burn metrics and have a bounded scroller. These contracts have synthetic/static-markup proof; native UI acceptance remains open.
- The browser-demo notch uses viewport height when no native work-area height is supplied. The card and strip scroll independently; the 36-pixel charcoal strip has 22-pixel rings containing provider marks, percentages below. Settings is available in each expanded card footer, with no strip button or reserved slot. The 220-pixel detail card plus strip use 256 pixels total. Synthetic browser checks cover scrolling, two card heights and both CSS card sides, but do not verify native window placement or display changes.
Expand Down Expand Up @@ -257,9 +270,10 @@ Release documentation refresh: 2026-10-17; build 61 executable inputs are unchan
- [`docs/DEMO.md`](DEMO.md): synthetic demo mode.
- [`docs/SIDE-NOTCH.md`](SIDE-NOTCH.md): side notch states, anchor and geometry invariants, interaction, and verification.
- [`docs/adr/0006-managed-account-list.md`](adr/0006-managed-account-list.md): current managed-account decision.
- [`docs/adr/0010-macos-typescript-electron.md`](adr/0010-macos-typescript-electron.md): macOS migration and Windows hold.
- [`docs/adr/0010-macos-typescript-electron.md`](adr/0010-macos-typescript-electron.md): macOS migration and the historical Windows hold, amended by ADR 0033 for source implementation.
- [`docs/mockups/README.md`](mockups/README.md): Wave 1–3 concept mockups; provider marks stay.
- [`docs/plans/feature-and-windows-pipeline.md`](plans/feature-and-windows-pipeline.md): superseded platform plan, current macOS direction and Windows hold.
- [`docs/adr/0033-windows-desktop-candidate.md`](adr/0033-windows-desktop-candidate.md): Windows candidate scope and remaining proof.
- [`docs/plans/feature-and-windows-pipeline.md`](plans/feature-and-windows-pipeline.md): current macOS direction, Windows candidate and open gates.
- [`Package.swift`](../Package.swift): package targets and platform requirement.
- [`CONTRIBUTING.md`](../CONTRIBUTING.md): contribution and validation commands.
- [`CHANGELOG.md`](../CHANGELOG.md): repository release-note history.
Expand Down
17 changes: 14 additions & 3 deletions docs/PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ The published Swift app retains the existing local/CLI boundaries. The
TypeScript/Electron candidate adds the opt-in boundaries described below. The candidate has source/fixture checks, but native behavior and
cutover acceptance remain pending. See [ADR 0010](adr/0010-macos-typescript-electron.md).

The Windows candidate stores preferences, provider history and sanitized API
billing snapshots under `%APPDATA%\MeterUsage`, normally the user's
`AppData\Roaming\MeterUsage`. It follows the redirected `%APPDATA%` location
when set. Windows uses the user's AppData access controls; it does not read
macOS UserDefaults or Keychain items. OpenAI and Anthropic billing accept a
one-time key in Settings, held in memory for that request. Windows has no saved
Keychain-key import. The compact tray is fixed on Windows, PNG sharing uses the
Save dialog, and automatic update installation is unavailable.

## Provider inputs and requests

| Provider | Read or request | Retained data |
Expand Down Expand Up @@ -213,7 +222,8 @@ compatibility and packaged acceptance. See [setup](DESKTOP-CONNECTIONS.md).
## Optional API billing snapshots

OpenAI API and Anthropic API billing use separate normalized snapshots under
`Library/Application Support/MeterUsage/api-usage`. The optional bundled helper
`Library/Application Support/MeterUsage/api-usage` on macOS and
`%APPDATA%\MeterUsage\api-usage` on Windows. The optional bundled helper
reads the selected organization Admin key from its environment for one fetch,
removes that key from its own environment and calls only the documented
OpenAI `/v1/organization/costs` or Anthropic `/v1/organizations/cost_report`
Expand All @@ -234,8 +244,9 @@ reject redirects, cap response size and pagination, and never start inference.

The helper writes only schema/provider, capture/period timestamps, currency and
daily USD totals. Identifiers, headers and raw responses/errors are discarded.
Files and their final directory are owner-only; replacement is atomic and a
failed request preserves the previous file. The app validates the fixed files
On macOS, files and their final directory are owner-only. On Windows, files
inherit the user's AppData access controls. Replacement is atomic and a failed
request preserves the previous file. The app validates the fixed files
and publishes only projected fields. Demo and isolated provider tests never
read live API billing files. Demo, candidate and isolated profiles cannot use
the Settings fetch action. Polling rereads snapshots and never fetches billing.
Expand Down
Loading
Loading