Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
63af84d
feat(history): unified calendar with intensity levels, hover day card…
StevenACZ Sep 18, 2026
659b669
feat(updates): in-panel update banner with download progress and defe…
StevenACZ Sep 18, 2026
e36350c
Merge branch 'wt/calendar' into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
578b6d0
feat(quota): adaptive ring layout with legible single, grid and row v…
StevenACZ Sep 18, 2026
3991ca1
Merge branch 'wt/cards' into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
6ae96cd
refactor(history): split UsageHistoryStore, prepare schema once per p…
StevenACZ Sep 18, 2026
6bd6381
refactor(clients): async account client over BlockingWork, incrementa…
StevenACZ Sep 18, 2026
5b146fb
refactor(core): drop dead activity chart, share test fixtures and hex…
StevenACZ Sep 18, 2026
1ae90eb
merge: wt/history-store into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
9c333bf
merge: wt/clients into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
9bdf53d
merge: wt/hygiene into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
e5fc5d6
fix(quota,updates): shared ring width formula, ViewThatFits cards, pe…
StevenACZ Sep 18, 2026
d5ca614
merge: wt/fix-cards-updater into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
609b684
refactor(store): split UsageStore into AppPreferences, RefreshSchedul…
StevenACZ Sep 18, 2026
62494a0
merge: wt/prefs into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
f58630f
perf(capture): single-pass incremental transcript scan with persisted…
StevenACZ Sep 18, 2026
40c53b8
merge: wt/scan into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
969ce80
fix(history): calendar-only unified scope, pinned card event monitor,…
StevenACZ Sep 18, 2026
da1f241
merge: wt/fix-calendar into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
7351c47
refactor(clients): concurrent history and account fetch, async refres…
StevenACZ Sep 18, 2026
b4e1a48
merge: wt/followup into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
0d13788
perf(history): keep one dashboard model per status item, single usage…
StevenACZ Sep 18, 2026
a3d8bc0
merge: wt/followup-h into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
e621bb1
fix(panel): standard style drops its footer and fits every quota card
StevenACZ Sep 18, 2026
d821ba5
merge: wt/fix-standard-height into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
d2b31ec
fix(services): guard deferred install, keep refresh busy through arch…
StevenACZ Sep 18, 2026
8a4eefe
merge: wt/fix-services into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
18d8fc4
fix(core): close read handles on schema errors, anchor scan resume an…
StevenACZ Sep 18, 2026
9c95983
merge: wt/fix-core into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
ff90777
fix(panel): size quota cards for real three-window data and keep the …
StevenACZ Sep 18, 2026
e915b05
merge: wt/fix-wave3 into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
0d6040a
chore(release): prepare version 1.4.0
StevenACZ Sep 18, 2026
de273a7
merge: wt/docs-1.4 into feat/1.4-ux-calendar-updater
StevenACZ Sep 18, 2026
a0660c8
docs(images): regenerate panel screenshots for 1.4.0
StevenACZ Sep 18, 2026
4729b33
fix(updates): keep a banner download at the install prompt instead of…
StevenACZ Sep 18, 2026
a1a93ce
fix(panel): keep provider cards unclipped under the update banner and…
StevenACZ Sep 18, 2026
20f3927
Merge wt/fix-updater-flow: banner download stops at the install prompt
StevenACZ Sep 18, 2026
1e0b106
Merge wt/fix-views-qa: banner never clips provider cards, Escape dism…
StevenACZ Sep 18, 2026
d498708
fix(updater): keep the ready title on the deferred banner
StevenACZ Sep 18, 2026
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
- Panel style and menu-bar style persist independently; existing installs default to classic panels and numeric indicators. Compact/ring styles retain exact quota and reset data, historical provenance, and provider/window selection. Graphics never show stale values as live; exact menu values remain in tooltip/accessibility. Animations are brief, event-driven, optional, and honor Reduce Motion.
- Use the native segmented control for history modes so selection and labels share one renderer. Year/week transitions must be checked in actual pixels after parent resizing; aligned accessibility frames alone cannot prove that the selection background stayed attached.
- Ring grids must cap columns at the number of visible windows and distribute the occupied row across the card. Two quotas must never leave a reserved third slot; cover this with a wide two-cell render check. Unified ring width follows visible quotas within its bounds.
- Centralize visual constants in `Theme.swift`; individual panels are 340 pt wide and Unified is 560 pt wide; the panel grows to fit its content, with the layout matrix verified below 640 pt. Never wrap the entire popover in a vertical scroll view; only unusually long provider lists scroll within their own bounded area. Individual views show only their own provider; Unified shows two titled cards side by side. Scroll only when real provider content exceeds its bounded height.
- Centralize visual constants in `Theme.swift`; individual panels are 340 pt wide and Unified is 560 pt wide; the panel grows to fit its content, with the layout matrix verified below 700 pt. Never wrap the entire popover in a vertical scroll view; only unusually long provider lists scroll within their own bounded area. Individual views show only their own provider; Unified shows two titled cards side by side. Scroll only when real provider content exceeds its bounded height.
- The popover closes on any click outside it: `.transient` alone does not dismiss an accessory app's popover when the click lands in another application (Steven, 2026-08-27). `StatusItemController` arms a global mouse-down monitor plus `didResignActiveNotification` while the popover is shown and tears both down in `popoverDidClose`. Keep the monitor to MOUSE events only — a global key monitor would demand Accessibility, which this app must never request.
- Reset lines scale with distance: under an hour shows minutes plus the clock time, under a day shows hours plus the clock time, the next calendar day shows `mañana` plus the clock time, and anything further shows whole days only. Far distances count the real remaining duration, never midnight crossings — 3.4 days away reads `3 d`, not `4 d`.
- The all-models weekly row never repeats a family that already owns its own scoped weekly row: `ProviderCard` passes those families to `ModelActivity.chips(excludingFamilies:)`, so `Semanal` and `Fable semanal` report disjoint token totals.
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.4.0] - 2026-09-18

### Added

- Quota ring cards that switch between one large ring, a grid, or compact rows according to the visible windows and the available width, keeping three Claude windows with model chips and pace lines fully readable.
- A calendar day card, shown by holding the pointer over a day or pinned with a click, with per-provider icons, tokens, percentages, model detail, and current and best streaks.
- Five calendar intensity levels derived from usage quantiles, so more color means more usage.
- An in-panel update banner with real download and extraction progress, followed by Install now or Later.
- A persistent Install button in the panel and in About after choosing Later, preserved across relaunches.

### Changed

- The yearly calendar now shows combined activity in the Codex, Claude, and Unified views.
- Calendar selection follows the pointer and returns to today when the pointer leaves the calendar; Esc, a click outside, scrolling, or changing view dismisses a pinned day card.
- The standard panel moves Settings, About, and Quit into a header gear menu instead of footer rows.
- Manual update checks are available in About whenever no update is in progress.
- Faster refreshes: transcripts are scanned incrementally with a resume file, a single capture pass replaces two, Claude activity and account data are read concurrently within a 12-second network limit, and each provider's card appears as soon as its own data is ready.
- Update feed overrides are accepted only over HTTPS.

### Fixed

- Claude readings, history, and pace continuity are scoped to the signed-in account, and Claude helper commands run in a private working directory.
- A refresh stays marked as busy until local archiving finishes.
- Local history read handles are closed when the database reports a schema error.
- Daily totals use the Gregorian calendar regardless of the system calendar setting.

## [1.3.0] - 2026-09-13

### Added
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ TokenGauge lives in your Mac’s menu bar and shows how much Claude Code or Code
- **A readable menu bar:** independently choose percentages, mini bars, or mini rings, with Large, Medium, or Small sizing. Exact percentages remain available on hover and in the panel.
- **Quiet by design:** optional brief transitions respect Reduce Motion; menu indicators redraw only when their data or appearance changes.
- **Separate limits:** general weekly quota and additional provider windows stay separate. Hide Luna weekly reserve in Settings if you do not use it.
- **Browsable activity:** rolling seven days, Monday-based weeks with previous/next navigation, or a yearly contribution calendar. Hover or select a day to inspect totals and available model/effort detail. In Unified, calendar colors show the provider with more recorded tokens, and intensity shows combined daily usage; ties use both colors. Separate month blocks and highlighted weekend rows make the year easier to scan.
- **Browsable activity:** rolling seven days, Monday-based weeks with previous/next navigation, or a yearly calendar that always shows Codex and Claude together, whichever view you are in. Five intensity levels show how much you used that day, colored by the provider with more recorded tokens. Hold the pointer over a day or click it to pin a card with per-provider tokens, percentages, model detail, and your current and best streaks; Esc, a click outside, scrolling, or changing view closes it. Separate month blocks and highlighted weekend rows make the year easier to scan.
- **Local history:** SQLite preserves daily totals and observed model/reasoning effort metadata after provider logs are trimmed. Missing days remain distinct from recorded zero usage.
- **Hourly quota pace:** an optional estimate in percentage points per hour for each weekly limit, based on verified recent readings, with a short explanation and dated last-active measurement.
- **English and Spanish:** follows your system language initially, with an explicit language choice in Settings.
- **Native updates:** daily checks, an inline install action, and manual checks in About.
- **Native updates:** daily checks, an in-panel banner with download progress, and manual checks in About.
- **No telemetry, ads, or model requests.**

## Install and connect
Expand All @@ -58,7 +58,7 @@ TokenGauge is a usage viewer, not a replacement for either provider. On a Mac wi

### Updates

Release builds check for updates daily. When one is available, choose **Install update** in the panel or **About TokenGauge**. Download progress appears inline, then the app relaunches. Automatic checking can be disabled in Settings; manual checks live in About. There are no separate Sparkle update windows.
Release builds check for updates daily. When one is available, a banner appears in the panel with download and extraction progress, then offers **Install now** or **Later**. Installing relaunches the app. If you choose Later, an Install button stays in the panel and in **About TokenGauge** until you use it, including after restarting the app. Automatic checking can be disabled in Settings; a manual check is available in About whenever no update is in progress. There are no separate Sparkle update windows.

Updates use [Sparkle](https://sparkle-project.org), a signed ZIP, and an EdDSA-signed appcast on GitHub Releases. Development builds stay off the public update channel.

Expand Down
4 changes: 2 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.3.0</string>
<string>1.4.0</string>
<key>CFBundleVersion</key>
<string>4</string>
<string>5</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
Expand Down
7 changes: 4 additions & 3 deletions Sources/TokenGaugeApp/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusController: StatusItemController?

func applicationDidFinishLaunching(_ notification: Notification) {
signal(SIGPIPE, SIG_IGN)
NSApp.setActivationPolicy(.accessory)
statusController = StatusItemController(store: store, launchAtLogin: launchAtLogin)
requestClaudeRecoveryConsent()
Expand All @@ -27,9 +28,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}

private func requestClaudeRecoveryConsent() {
guard UserDefaults.standard.object(forKey: "claudeAutomaticRecovery") == nil,
ClaudeSessionRecovery.executable() != nil
else { return }
guard !AppPreferences().claudeAutomaticRecoveryDecided, ClaudeSessionRecovery.executable() != nil else {
return
}
let alert = NSAlert()
alert.messageText = "setup.claude_recovery_title".localized
alert.informativeText = "setup.claude_recovery_message".localized
Expand Down
12 changes: 11 additions & 1 deletion Sources/TokenGaugeApp/App/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ final class StatusItemController: NSObject {
private var popoverSizeObservation: NSKeyValueObservation?
private var positioningUpdatePending = false
private var positionedGeometry: PopoverGeometry?
private var history: HistoryDashboardModel?

private struct PopoverGeometry: Equatable {
let sourceWindow: NSRect
Expand Down Expand Up @@ -97,7 +98,7 @@ final class StatusItemController: NSObject {
},
showAbout: { [weak self] in
self?.showAbout()
})
}, history: historyModel())
let controller = NSHostingController(rootView: view)
controller.sizingOptions = [.preferredContentSize]
popover.contentViewController = controller
Expand All @@ -106,6 +107,14 @@ final class StatusItemController: NSObject {
startDismissMonitors()
}

private func historyModel() -> HistoryDashboardModel {
if let history { return history }
let preview = store.historyReadsEnabled ? nil : [store.claude.snapshot, store.codex.snapshot].compactMap { $0 }
let model = HistoryDashboardModel(previewSnapshots: preview, mode: store.historyMode)
history = model
return model
}

private func schedulePopoverPositionUpdate() {
guard popover.isShown, !positioningUpdatePending else { return }
positioningUpdatePending = true
Expand Down Expand Up @@ -201,5 +210,6 @@ extension StatusItemController: NSPopoverDelegate {
positionedGeometry = nil
stopDismissMonitors()
popover.contentViewController = nil
history?.popoverDidClose()
}
}
23 changes: 19 additions & 4 deletions Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,19 @@
"updates.checking" = "Checking for updates…";
"updates.current" = "You’re up to date";
"updates.check_failed" = "Could not check. Try again";
"updates.install" = "Install update · %@";
"updates.downloading" = "Downloading update…";
"updates.installing" = "Installing and restarting…";
"updates.retry" = "Update failed. Retry";
"updates.banner_available" = "TokenGauge %@ available";
"updates.download" = "Download";
"updates.banner_downloading" = "Downloading %1$@ · %2$d%%";
"updates.banner_downloading_pending" = "Downloading %@…";
"updates.banner_extracting" = "Preparing %1$@ · %2$d%%";
"updates.banner_extracting_pending" = "Preparing %@…";
"updates.banner_ready" = "%@ is ready to install";
"updates.install_now" = "Install now";
"updates.later" = "Later";
"updates.install_version" = "Install %@";
"updates.failed" = "The update could not be installed";
"updates.retry_action" = "Retry";
"about.description" = "Your Claude Code and Codex usage, at a glance. Quotas, reset times and activity from the menu bar.";
"about.local" = "Local history";
"about.private" = "Private by design";
Expand All @@ -93,7 +102,6 @@
"setup.claude_session" = "Uses your local Claude session";
"updates.local" = "Local installation";
"settings.mark_cancelled" = "Subscription cancelled";
"activity.selected_scoped" = "%@: %@";
"view.unified" = "Unified";
"settings.display_mode" = "Menu bar view";
"settings.display_mode_help" = "Choose one provider or both in the menu bar and panel.";
Expand All @@ -114,6 +122,7 @@
"status.credential_expired" = "Connection pending";
"message.credential_expired" = "The shared session expired. Enable automatic reconnection in Settings or open Claude Code.";
"quota.last_remaining" = "last remaining";
"quota.last_remaining_value" = "%@ last remaining";
"settings.claude_recovery" = "Automatic reconnection";
"settings.claude_recovery_help" = "Allow Claude Code to start briefly in the background when its session expires. No messages or model tokens. Requires an existing Claude Code login.";
"setup.claude_recovery_title" = "Allow automatic Claude reconnection?";
Expand Down Expand Up @@ -166,6 +175,12 @@
"history.equal_use" = "Equal recorded usage for both providers";
"history.weekends" = "Saturdays and Sundays";
"history.period" = "History period";
"history.streak" = "Streak %1$@ d · best %2$@ d";
"history.streak_current" = "Current streak: %@ d";
"history.streak_longest" = "Longest streak: %@ d";
"history.share" = "%@ of the day's total";
"history.no_activity" = "No recorded activity";
"history.close" = "Close";
"pace.last_value" = "Last ≈ %@ pp/h";
"pace.info_title" = "Weekly usage pace";
"pace.info_explanation" = "Estimates how quickly you use this allowance. For example, 5 pp/h means going from 80% to 75% remaining in one hour.";
Expand Down
23 changes: 19 additions & 4 deletions Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,19 @@
"updates.checking" = "Buscando actualizaciones…";
"updates.current" = "Tienes la última versión";
"updates.check_failed" = "No se pudo comprobar. Reintentar";
"updates.install" = "Instalar actualización · %@";
"updates.downloading" = "Descargando actualización…";
"updates.installing" = "Instalando y reiniciando…";
"updates.retry" = "La actualización falló. Reintentar";
"updates.banner_available" = "TokenGauge %@ disponible";
"updates.download" = "Descargar";
"updates.banner_downloading" = "Descargando %1$@ · %2$d %%";
"updates.banner_downloading_pending" = "Descargando %@…";
"updates.banner_extracting" = "Preparando %1$@ · %2$d %%";
"updates.banner_extracting_pending" = "Preparando %@…";
"updates.banner_ready" = "%@ lista para instalar";
"updates.install_now" = "Instalar ahora";
"updates.later" = "Después";
"updates.install_version" = "Instalar %@";
"updates.failed" = "No se pudo instalar la actualización";
"updates.retry_action" = "Reintentar";
"about.description" = "Tu uso de Claude Code y Codex, de un vistazo. Cuotas, reinicios y actividad desde la barra de menú.";
"about.local" = "Historial local";
"about.private" = "Privado por diseño";
Expand All @@ -93,7 +102,6 @@
"setup.claude_session" = "Usa tu sesión local de Claude";
"updates.local" = "Instalación local";
"settings.mark_cancelled" = "Suscripción cancelada";
"activity.selected_scoped" = "%@: %@";
"view.unified" = "Unificado";
"settings.display_mode" = "Vista de la barra";
"settings.display_mode_help" = "Elige un proveedor o ambos para la barra y el panel.";
Expand All @@ -114,6 +122,7 @@
"status.credential_expired" = "Conexión pendiente";
"message.credential_expired" = "La sesión compartida caducó. Activa la reconexión automática en Configuración o abre Claude Code.";
"quota.last_remaining" = "último saldo";
"quota.last_remaining_value" = "%@ último saldo";
"settings.claude_recovery" = "Reconexión automática";
"settings.claude_recovery_help" = "Permite iniciar Claude Code brevemente en segundo plano si su sesión caduca. Sin enviar mensajes ni consumir tokens. Requiere haber iniciado sesión en Claude Code.";
"setup.claude_recovery_title" = "¿Permitir la reconexión automática de Claude?";
Expand Down Expand Up @@ -166,6 +175,12 @@
"history.equal_use" = "Mismo uso registrado en ambos proveedores";
"history.weekends" = "Sábados y domingos";
"history.period" = "Periodo del historial";
"history.streak" = "Racha %1$@ d · mejor %2$@ d";
"history.streak_current" = "Racha actual: %@ d";
"history.streak_longest" = "Mejor racha: %@ d";
"history.share" = "%@ del total del día";
"history.no_activity" = "Sin actividad registrada";
"history.close" = "Cerrar";
"pace.last_value" = "Último ≈ %@ pp/h";
"pace.info_title" = "Ritmo de uso semanal";
"pace.info_explanation" = "Estima qué tan rápido consumes esta cuota. Por ejemplo, 5 pp/h equivale a pasar de 80% a 75% libre en una hora.";
Expand Down
Loading
Loading