Skip to content
Closed
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
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ follows [Semantic Versioning](https://semver.org/).

### Added

- Add Small, Medium and Large panel sizes in candidate Settings. The choice
persists and applies to main-panel and Settings text and controls. Main-panel
width scales with the choice; its height fits content up to the display edge.
Small uses 80% scale and a 336-point main-panel width before display clamping.
Ctrl/Cmd plus and minus select these sizes; Ctrl/Cmd 0 restores Medium.
These shortcuts keep the content scale, saved choice and main-panel fit together.

- Add an isolated Codex cloud test profile for the existing candidate UI. It
starts disconnected, reads account allowance after browser sign-in, and
excludes demo data, local activity and other providers. Candidate profiles
cannot check or install updates, change login items or send notifications.
Profile validation rejects nested symlinks before app or helper writes.

- Add candidate account setup for Codex browser sign-in and opt-in Claude
Desktop allowance, with automatic refresh, account isolation and disconnect.
Grok consumer connection remains unavailable. Native and provider acceptance
are pending; the published Swift app is unchanged.

- Add a macOS TypeScript/Electron/React migration candidate and headless
schema-1 JSON CLI, with synthetic provider, account, history, pacing and IPC
tests. Use pnpm, Vite+, Tailwind, Vitest and electron-builder. Retain Swift as
Expand All @@ -16,6 +34,22 @@ follows [Semantic Versioning](https://semver.org/).

### Changed

- Move candidate onboarding into Settings. First-time setup reuses account
connections and allows Done without signing in. Done saves completion before
returning to usage; a save failure keeps setup open. The main panel shows usage
without the welcome and account-setup card.

- Group candidate Settings by task, show six accent color swatches, and label
provider collection separately from menu-bar and side-notch display.
Turning collection off retains the saved display preference.
Appearance sits below account connections, with visible size choices,
grouped rows and aligned provider controls. Settings content scales with the
size choice while its window frame stays fixed.

- Label local activity as activity on this Mac, keep folder-based accounts
under advanced settings, and omit local CLI activity from separately
connected account cards. Record the new opt-in authentication boundary.

- Record synthetic Claude and Codex source-scanner timings alongside the Swift
baseline. Counts and cache transitions pass; packaged-app performance, idle
RSS and native GUI acceptance remain open.
Expand All @@ -32,6 +66,41 @@ follows [Semantic Versioning](https://semver.org/).

### Fixed

- Handle opening an already-running candidate without an uncaught startup
popup. Validate Electron's singleton metadata before requesting its lock,
retain all other symlink checks, and reveal the existing panel after startup.
Invalid profiles exit with a fixed error; activation cannot reopen a panel
during Quit.

- Stop display updates when Quit begins so a late usage refresh cannot reopen
the side notch or restart update checks during shutdown.

- Launch Codex account helpers with the documented stdio transport. Keep
interpreter search paths available without inheriting credential variables.
- Keep provider display controls available for active additional accounts when
primary collection is off, and isolate connection-test helper discovery.

- Bound browser-demo notch cards and strips to the viewport height. Keep
provider rings at full width while the strip scrolls.
- Hide scrollbar indicators throughout the candidate while retaining scrolling.
- Ignore display and theme publications after the candidate tray is destroyed.
- Retain each window's document ID before destruction so closing a candidate
window does not access destroyed web contents or remove its replacement.

- Reject malformed connection provider values before consent and target the
bundled Desktop Keychain helper explicitly at arm64/macOS 13.
- Disable cancelled or disconnected account collection before cleanup, recover
unfinished sign-in profiles, and verify Claude identity after HTTP failures.
Preserve connection readers across cache clearing and headless reports.
- Stop Claude credential rereads after disconnect, discard unverified account
allowance on collection timeout, and retry retained Codex cleanup from the
sign-in button before opening a new login.
- Show general weekly Claude allowance, hide unsupported reset actions, clear
replaced-account alert suppression, and exclude remote usage from local totals.

- Check repaired cache token fields directly in the synthetic regression test;
a raw substring check could also match an unrelated timestamp or opaque ID.

- Rescan corrupt Claude cache entries with non-finite costs or unsupported
dates. Ignore unsupported optional provider timestamps before JSON output.
- Use canonical temporary paths in candidate fixtures and demo commands.
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ 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.

`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
macOS 13 explicitly, matching the candidate bundle. Verify the packaged helper's
architecture and deployment target with `file` and `vtool -show-build` on macOS.
The same build command prepares an unpackaged native candidate. Do not invoke the helper during synthetic
QA; it reads an actual Keychain item. Desktop connections are disabled in demo
mode. Live connection acceptance remains separate from source checks.

```sh
pnpm install --frozen-lockfile
pnpm typecheck
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ and React. The candidate uses pnpm, Vite+, Tailwind, Vitest and electron-builder
The downloadable 0.2.41 app above remains the Swift release. The Electron
candidate has not passed native macOS acceptance and is not a published release.

The candidate also has [desktop account connection controls](docs/DESKTOP-CONNECTIONS.md)
for Codex browser sign-in and opt-in Claude Desktop allowance. Setup uses buttons
and approval dialogs. Grok consumer connection remains unavailable. Native
sign-in, provider responses and Keychain acceptance still require verification.

Provider readers and credentials stay in the main process. The sandboxed
renderer receives validated snapshots through a small IPC bridge. Existing
history/archive JSON, preference keys, managed account IDs and provider marks
Expand Down
21 changes: 21 additions & 0 deletions Scripts/electron/desktop-keychain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import LocalAuthentication
import Security

// The helper has one fixed read. Background polling cannot open an auth dialog.
let context = LAContext()
context.interactionNotAllowed = !CommandLine.arguments.contains("--allow-ui")
SecKeychainSetUserInteractionAllowed(!context.interactionNotAllowed)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "Claude Safe Storage",
kSecAttrAccount as String: "Claude Key",
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationContext as String: context
]
var result: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
let password = result as? Data, !password.isEmpty else { exit(1) }
// Only the owning main process receives stdout. Never run this helper for QA.
FileHandle.standardOutput.write(password)
128 changes: 128 additions & 0 deletions docs/DESKTOP-CONNECTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Desktop account connections

The macOS Electron candidate keeps account connection controls in Settings,
including first-run setup. This is candidate source behavior, not support in the
published Swift app. Native sign-in, Keychain permission, packaging and live
provider acceptance remain unverified.

## Setup

Before setup is complete, launch or app activation opens Settings. Connect an
account there, or choose **Done** to continue without signing in. Done saves the
existing setup-completion preference before opening usage. If saving fails,
Settings keeps the setup guidance and reports the error. Account connections
remain in Settings after setup; the main usage panel has no welcome card.
See [ADR 0014](adr/0014-settings-onboarding.md).

- Codex: choose **Sign in to Codex** and complete browser sign-in. The candidate
finds the helper in an existing Codex app in Applications, with existing
command installations as a compatibility path. Users do not open a terminal
or choose a configuration folder. A missing helper produces an unavailable
message. The Codex app's authentication is not changed.
- Claude: sign in to Claude Desktop on the Mac, choose **Connect Claude
Desktop**, and approve MeterUsage's access explanation and any macOS
Keychain prompt. The candidate requests account allowance using Desktop's
current access token. This interface can change and is not a documented
third-party consumer API contract.
- Grok: automatic consumer connection is unavailable. Existing command-based
collection remains available for existing users. It does not establish
browser/mobile consumer support. The setup does not ask for manual numbers.

The normal refresh interval applies after connection. An expired login,
unsupported cache format, missing permission or provider failure keeps usage
unavailable. Open the relevant desktop app, disconnect the MeterUsage
connection, then connect again. Disconnect removes MeterUsage's own connection;
it does not sign out or erase data in another app.

## Data coverage

Account allowance comes from the provider account service. It can reflect use
outside the Mac where the provider shares that allowance. Provider account,
plan and product rules determine coverage. A quota percentage is not a token
ledger, message total or subscription invoice.

Local activity charts are labelled as activity on this Mac. OpenRouter's
remote usage is labelled as account activity and excluded from local totals. Connected
Codex/Claude account cards omit the separate local CLI activity source because
MeterUsage cannot establish that it belongs to the connected account. No local
and account total is added together. Detailed account token history, ordinary
Claude chat tokens and Grok consumer history are outside this implementation.

## Authentication and isolation

Codex uses a generated MeterUsage-owned helper profile. Before login or polling,
the candidate checks the effective helper configuration for `keyring` storage.
It refuses another mode, including plaintext-file fallback. Login, allowance
reads and own-profile logout use the helper's `--listen stdio://` transport.
The helper environment keeps interpreter search paths without forwarding
inherited credential variables. The browser URL
must use HTTPS on an approved OpenAI host. Disconnect invokes logout only for
that generated profile. Cancellation attempts the same cleanup; a cleanup
failure is reported.

An unfinished sign-in profile enables cleanup only. Cancellation and disconnect
disable collection before logout, even if logout fails. The candidate retains
the owned profile ID for cleanup on the next GUI launch or explicit sign-in
retry. Retry completes cleanup before starting another login. Normal quit waits
up to five seconds for authentication cleanup. Headless reports only read existing
connections; they never start sign-in, show a permission prompt or perform
cleanup logout. Clear local scan cache keeps the established connection reader.
Connected account cards show reset-credit details without redemption controls;
redemption stays limited to existing sources that support it.

Claude access requires explicit consent. A bundled native helper performs one
fixed Keychain read. Background calls forbid interactive authentication. The
main process decrypts the Desktop cache, reads the active organization cookie
from a read-only database, and selects one valid account-scoped access token.
Ambiguous, legacy, expired or unsupported caches fail closed. Refresh tokens
are skipped, and MeterUsage never writes to Claude Desktop's stores. It uses
its own User-Agent and requests no reset grants or model inference.

Only an opaque account/organization fingerprint is retained for Claude. It is
not published to the renderer. The candidate checks it before and after the
allowance request so a Desktop account switch cannot update the old account
card. Switching the connection clears prior quota. Connected-account quota
archives are discarded at launch, and authentication/data errors remove them.
This avoids restoring a reading without proving its account identity.
If an allowance request fails, the current account is checked again before a
dated last-known reading can remain. Missing or revoked Desktop access clears
that reading. A collection timeout clears account-bound quota because the
identity check may not have completed. Disconnect prevents the post-request
credential reread. Changing a connection also clears that slot's alert suppression.

Demo mode blocks live account connections, Keychain access and helper login.
Synthetic tests cover protocol, consent, token selection, account switching,
disconnect, numeric validation, stale source results and IPC boundaries.
See [privacy](PRIVACY.md) and [ADR 0011](adr/0011-desktop-account-connections.md).

## Isolated live Codex validation

The candidate accepts `--codex-test-profile` with an absolute test directory.
This selects the existing UI and connection flow with live Codex allowance.
It cannot be combined with `--demo` or `--candidate-profile`. The directory
must be empty on first use and owned by this test mode on later launches.
Installed-app storage and symlink paths are refused, including nested paths
inside an existing profile. The macOS GUI permits only Electron's three direct
singleton links after checking their ownership, socket location and matching
metadata. JSON entrypoints retain strict symlink rejection. Startup checks
metadata before requesting Electron's lock or starting the helper, with a
limit of 32,768 entries and 24 directory levels. A second launch reveals the
existing panel; invalid metadata exits without an uncaught popup. See
[ADR 0013](adr/0013-candidate-single-instance-startup.md).

Preferences, history, connection profiles and Electron state stay in that
directory. The real OS home remains available for helper discovery and macOS
Keychain. Choose **Sign in to Codex** in the app and finish browser sign-in.
The app starts disconnected, so an unavailable reading before sign-in is
expected. Successful connections remain available across normal quit and
relaunch; **Disconnect** signs out only that MeterUsage connection.

This mode reads only Codex account allowance. It has no local activity, status
feed, reset redemption or other-provider collection. Update checks, update
installation, login-item changes and notifications are disabled. It does not
change the primary setup for users or replace an installed app.

Synthetic isolation tests do not prove native sign-in or visible cloud usage.
Live acceptance requires the packaged app, human browser sign-in, fresh
allowance in its actual UI and comparison with the same provider account.
See [ADR 0012](adr/0012-codex-live-test-profile.md).
Loading