feat(cli): show Session pace and add pace to JSON output - #10
feat(cli): show Session pace and add pace to JSON output#10kmatsunami wants to merge 13 commits into
Conversation
kmatsunami
left a comment
There was a problem hiding this comment.
Code review — Session pace + JSON pace
Reviewed at f7cf2f0 (xhigh-effort pass: 10 finder angles + verification + gap sweep). No functional bugs — no crashes, nil-derefs, broken call sites, dropped guards, or broken existing tests. pace is correctly added to the explicit CodingKeys; the synthesized encoder correctly omits nil keys; the rounding update makes the summary string and the numeric fields agree.
The one cross-cutting theme: the code comments say the output "mirrors the GUI," and that holds for session pace (computePace(.session) matches UsagePaceText.sessionPace exactly). It does not hold for weekly pace — the CLI uses plain linear UsagePace.weekly while the menu's UsageStore.weeklyPace applies work-days, Codex historical refinement, and a broader provider set. That weekly gap was pre-existing in the text output, but this PR now publishes it as a machine-readable JSON number, so it's worth being explicit about. Everything else is maintainability (the pace logic now lives in three independent copies).
Nothing here blocks the PR. Inline notes below, most-impactful first.
| if provider == .ollama, window.windowMinutes == nil { return nil } | ||
| guard window.remainingPercent > 0 else { return nil } | ||
| guard let pace = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 10080) else { return nil } | ||
| guard let pace = UsagePace.weekly( |
There was a problem hiding this comment.
Weekly pace diverges from the menu it claims to mirror. This calls plain linear UsagePace.weekly(...) with no workDays and no Codex historical refinement, whereas the GUI's UsageStore.weeklyPace (UsageStore+HistoricalPace.swift:9) passes settings.weeklyProgressWorkDays and routes Codex through CodexHistoricalPaceEvaluator.
So for a Codex user with historical tracking on, or any user whose weeklyProgressWorkDays != 7, codexbar usage --format json pace.secondary (and the weekly text Pace line) report a different stage/deltaPercent/etaSeconds than the menu bar. The session path is faithful; only weekly diverges. Either route weekly through the same evaluator, or soften the "mirrors the GUI" wording to "mirrors the GUI's session pace; weekly is a plain linear approximation."
There was a problem hiding this comment.
Softened the PaceKind comment in 44a1afc: only .session claims GUI parity; .weekly is now documented as a plain linear approximation (no workDays / Codex historical / windowMinutes-only gating). Routing weekly through the GUI evaluator isn't possible from CLI (CodexBarCLI can't import CodexBar/UsageStore), so this is wording-only here; full parity tracked with C6 as a follow-up.
| case .session: | ||
| provider == .codex || provider == .claude || provider == .ollama | ||
| case .weekly: | ||
| provider == .codex || provider == .claude || provider == .opencode || provider == .ollama |
There was a problem hiding this comment.
Weekly provider coverage is narrower than the menu. This allowlist gates weekly pace to codex/claude/opencode/ollama, but UsageStore.weeklyPace shows a weekly Pace line for any provider whose secondary window carries windowMinutes (cursor, factory-weekly, gemini, …). Those providers get no pace.secondary in the JSON and no weekly text Pace line, even though the menu paces them. Pre-existing for the text path, but now also a silent gap in the JSON contract.
There was a problem hiding this comment.
Confirmed: GUI gates weekly on windowMinutes != nil for any provider (UsageStore+HistoricalPace.swift:32), CLI uses the codex/claude/opencode/ollama allowlist. Pre-existing in the text path; left unchanged here and the comment now documents the divergence. Dropping the allowlist to match the GUI rule is deferred to a follow-up (it changes existing text output too).
| title: labels.primary, | ||
| window: primary, | ||
| includePace: false, | ||
| paceKind: .session, |
There was a problem hiding this comment.
Heads-up on a corner case (matches the GUI, so likely intended, but new to the CLI): when a Claude account has no 5-hour data, ClaudeUsageFetcher falls the primary back to the 7-day window (windowMinutes = 10080, kind .usage — ClaudeUsageFetcher.swift:962). It survives the spend-limit nil filter and reaches .session here, so a pace computed over 7 days prints under the "Session" label with "Projected empty in 2d" wording. The math is correct (it uses the real 10080-min window), it just reads oddly as a "session." Mirrors UsagePaceText.sessionSummary(primary).
There was a problem hiding this comment.
Acknowledged — intended, matches UsagePaceText.sessionSummary(primary). No change.
| let willLastToReset: Bool | ||
| /// Seconds until the window is projected to be exhausted. Omitted when it lasts to reset or is unknown. | ||
| let etaSeconds: TimeInterval? | ||
| let runOutProbability: Double? |
There was a problem hiding this comment.
runOutProbability is a structurally-dead field for the CLI: every CLI pace flows through UsagePace.weekly, which hard-codes runOutProbability: nil (UsagePace.swift:113), and the CLI never calls UsagePace.historical. So this key is never populated, and the CLI summary can never carry the GUI's "≈ X% run-out risk" clause that UsagePaceText.detailRightLabel appends. Fine to keep for schema-parity, but worth a doc note that it's always absent in CLI output.
There was a problem hiding this comment.
Documented on the field in 44a1afc: runOutProbability is always absent in CLI output (CLI computes via UsagePace.weekly, never .historical). Kept for schema parity.
|
|
||
| /// Computes the pace for a window using the same guards/window length as the GUI, or nil when no | ||
| /// Pace line should be shown. Shared by the text renderer and the JSON payload so they stay in sync. | ||
| private static func computePace( |
There was a problem hiding this comment.
Maintainability / altitude (the big one). Pace logic now lives in three independent copies: this computePace byte-copies the guards of UsagePaceText.sessionPace; paceLeftLabel/paceRightLabel/paceDurationText copy detailLeftLabel/detailRightLabel/durationText; and UsageStore.weeklyPace is a third. The new shared helper only unifies CLI text+JSON — the GUI is still free to drift.
The magic numbers 300/10080/3 are likewise duplicated across CLIRenderer, UsagePace, UsagePaceText, and UsageStore, and stageString is a hand-written 7-case switch that must track UsagePace.Stage.
Deeper fix: lift a shared sessionPace/weeklyPace + the label builders + the constants into CodexBarCore (the only module both CodexBar and CodexBarCLI depend on), and make UsagePace.Stage a String-backed enum so any serializer shares one spelling. Reasonable to do as a follow-up, but flagging so the next pace edit doesn't silently desync CLI from the menu.
There was a problem hiding this comment.
Agreed; deferred to a follow-up issue: lift shared sessionPace/weeklyPace + label builders + the 300/10080/3 constants into CodexBarCore, and make UsagePace.Stage String-backed (removing the hand-written stageString). Out of scope for this PR per scope decision.
| } | ||
|
|
||
| @Test | ||
| func `renders session pace line when session window has reset`() { |
There was a problem hiding this comment.
Two test notes:
- Timing coupling: this and the other two
renderText-based pace tests build the snapshot with oneDate(), butrenderTextcaptures its ownDate()(CLIRenderer.swift:19), so the literal substring expectations (Expected 60% used) depend on rounding absorbing the inter-call drift. The margin to the rounding boundary here is ~90s so it won't flake in practice — but the JSON tests' fixed-nowpattern is the deterministic one; consider passingnowthrough torenderTextin these too. - Coverage gap: no test exercises the
onTrackboundary ("On pace", |delta| ≤ 2), thewillLastToReset→ "Lasts until reset" branch, or the eta-rounds-to-"now" wording. Every pace test uses delta 30/−40 and eta "in …", so a regression at those boundaries would ship green.
There was a problem hiding this comment.
44a1afc: (1) added a now parameter to renderText and threaded it into the exact-string pace tests, removing the inter-call Date() drift. (2) added an on-track / 'Lasts until reset' boundary test. The eta-rounds-to-'now' wording is effectively unreachable via the public path: resetCountdownDescription returns 'now' only for eta < 1s, but the expectedUsedPercent >= 3 guard forces elapsed >= 540s of the 300-min window, so the minimum reachable eta is ~5s ('in 1m'). Noted rather than adding an un-constructable test.
| openaiDashboard: OpenAIDashboardSnapshot?, | ||
| error: ProviderErrorPayload?) | ||
| error: ProviderErrorPayload?, | ||
| pace: ProviderPacePayload? = nil) |
There was a problem hiding this comment.
要ります。エラー経路の ProviderPayload 構築(CLIErrorReporting.swift:35/58/115・CLIHelpers.swift:325)と一部テストが pace 無しで生成しており、同じ init の既存 cacheAccountKey: String? = nil と同じ慣習です。外すと各呼び出しで pace: nil 明示が必要になり、cacheAccountKey と不揃いになります。
| openaiDashboard: OpenAIDashboardSnapshot?, | ||
| error: ProviderErrorPayload?) | ||
| error: ProviderErrorPayload?, | ||
| pace: ProviderPacePayload? = nil) |
There was a problem hiding this comment.
要ります。エラー経路の ProviderPayload 構築(CLIErrorReporting.swift:35/58/115・CLIHelpers.swift:325)と一部テストが pace 無しで生成しており、同じ init の既存 cacheAccountKey: String? = nil と同じ慣習です。外すと各呼び出しで pace: nil 明示が必要になり、cacheAccountKey と不揃いになります。
| } | ||
|
|
||
| struct PacePayload: Encodable { | ||
| /// Raw `UsagePace.Stage`: onTrack / slightlyAhead / ahead / farAhead / slightlyBehind / behind / farBehind. |
There was a problem hiding this comment.
並び順が変。farAheadからfarBehindまで順番に並べて
There was a problem hiding this comment.
0065cac8 で stageString を farAhead → ahead → slightlyAhead → onTrack → slightlyBehind → behind → farBehind のスペクトル順に並べ替えました。コメントが付いていた doc 行(/// Raw UsagePace.Stage: …)はコメント削減で除去済みのため、実コードの列挙である stageString を対象にしています。
kmatsunami
left a comment
There was a problem hiding this comment.
Re-review @ 44a1afc — all feedback addressed ✅
Verified locally: swift test --filter CLISnapshotTests → 34 passing, build clean, removed field has no orphan references. Walking the prior comments:
| Prior finding | Status |
|---|---|
| Weekly diverges from menu (no work-days / no Codex historical) | ✅ Documented honestly — PaceKind now states .session mirrors the GUI exactly while .weekly is "only a plain linear approximation … so weekly stage/delta/eta can differ from the menu bar." The behavioral gap is inherent (the CLI has no access to the GUI's account-scoped historical dataset or live weeklyProgressWorkDays), so documenting it precisely is the right call. |
| Weekly provider coverage narrower than menu | ✅ Same doc now calls out the "fixed provider allowlist rather than the menu's any window with windowMinutes rule." |
runOutProbability is a dead field |
✅ Doc added: "Always absent in CLI output … Kept for schema parity." Exactly the note I suggested. |
deltaPercent inconsistent after rounding |
✅ Fixed properly — actualUsedPercent is removed from the payload (it duplicated usage.<window>.usedPercent), so there's no longer a derivable field that disagrees with its parts. Tests updated to assert the key is omitted. |
| Timing-coupled text tests | ✅ Fixed — renderText now takes now: Date = Date(), and the three text pace tests thread now: now so the snapshot clock and render clock match. Deterministic now. |
| Boundary coverage gap | ✅ New test renders session pace on track and lasts until reset covers both the onTrack ("On pace", delta 0) boundary and the willLastToReset → "Lasts until reset" branch. |
| Claude 7-day fallback shown as "Session" | ⚪ Left as-is — intentional, mirrors UsagePaceText.sessionSummary(primary). Fine. |
LGTM. (Can't formally Approve my own PR, hence the comment.)
One tiny, non-blocking leftover for whenever: the eta-rounds-to-"now" wording (Projected empty now / Runs out now) is still the only pace branch without a test. And the larger refactor — lifting the shared session/weekly pace + label helpers + the 300/10080/3 constants into CodexBarCore, and making UsagePace.Stage String-backed — remains a good follow-up to keep CLI and menu from drifting, but is rightly out of scope for this PR.
f24122a to
bbb0b4a
Compare
0065cac to
c9074b3
Compare
`codexbar usage` already shows a Pace line for the Weekly window but not for Session, and `--format json` emits no pace at all. This fills both gaps so pace coverage is consistent across windows and output formats. - Text: add a Pace line under "Session" (previously Weekly-only), using the GUI's session-pace calculation (5-hour window; codex/claude/ollama) and its "Projected empty in …" wording; Weekly keeps "Runs out in …". - JSON: add a derived `pace` object — `pace.primary` (session) and `pace.secondary` (weekly) — with stage, deltaPercent, expectedUsedPercent, willLastToReset, optional etaSeconds, and a human-readable summary. Numbers are rounded to whole values to match the existing `usage` block; a key is emitted only when the corresponding text Pace line would render. Session pace applies only to real (≤5h) session windows, so a 7-day window that some providers fall back into `primary` (e.g. Claude with no 5-hour data) is not mislabeled as a session. Weekly remains a plain linear approximation (no workDays / Codex historical refinement), unlike the menu's UsageStore.weeklyPace. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c9074b3 to
84f35ce
Compare
The CLI weekly pace line was always linear (elapsed/duration), so it diverged from the menu bar whenever the user had configured a work-day count in Preferences > Display. The mismatch made the same number look like a larger deficit in the CLI than in the GUI. Root cause: `UsagePace.weekly()` already supports a `workDays:` parameter that narrows the baseline to elapsed/total work-day time only, but the CLI called it without `workDays` (nil → linear). Fix: read `weeklyProgressWorkDays` from the GUI app's UserDefaults (domains `com.steipete.codexbar` / `.debug`, fallback to standard) using the same lookup pattern as `resetTimeDisplayStyleFromDefaults()`. Pass the value through `UsageCommandContext → RenderContext / providerPacePayload → computePace`, where it is forwarded to `UsagePace.weekly` only for the `.weekly` pace kind (session/5h ignores it, as the workday correction in `UsagePace.weekly` requires minutes == 10080). When no setting is stored (Off / not configured) `weeklyWorkDays` is nil and the behaviour is unchanged (linear baseline, same as before). All 35 CLISnapshotTests pass.
* Fix remaining memory pressure source isolation crash * docs: note memory pressure crash fix --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
ProviderVersionDetector.claudeVersion() invoked the CLI as `claude --allowed-tools "" --version`. The `--allowed-tools` flag makes Claude Code treat the call as a real session and initialize its credential subsystem, reading the OAuth token from the macOS keychain item "Claude Code-credentials" via `/usr/bin/security`. When no `~/.claude/.credentials.json` file and no `CLAUDE_CODE_OAUTH_TOKEN` env var exist, that keychain read pops a `security`-subject keychain prompt on every version probe (i.e. every usage refresh), regardless of the Claude usage source or CodexBar's keychain prompt policy. Drop the `--allowed-tools ""` args so the probe runs plain `claude --version`, which prints the version and exits before any credential/keychain access (matching the codex/gemini detectors). Verified: `claude --allowed-tools "" --version` -> version + 1 keychain prompt; `claude --version` -> same version, 0 prompts.
Co-authored-by: kiranmagic7 <262980978+kiranmagic7@users.noreply.github.com>
* Fix stale today usage buckets * Handle overlapping API usage buckets * Avoid Admin API UTC bucket overcount * fix: keep Poe Today usage local-day scoped --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Fix Antigravity quota summary lanes * Align Antigravity icon lanes with quota summary ranking * fix: align Antigravity ranking with rendered lanes --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
What was missing
codexbaralready computes pace, but coverage was uneven:codexbar usage): a Pace line is shown for Weekly but not for Session.--format json): no pace information is emitted at all.This PR fills both gaps so pace is consistent across windows and output formats.
Changes
UsagePaceText.sessionPace; 5-hour window; codex/claude/ollama), with the GUI's "Projected empty in …" wording (Weekly keeps "Runs out in …").--format json): adds a derivedpaceobject to each provider payload.docs/cli.mdsample output (text + JSON) to reflect the new Session pace line andpaceobject.Sample output
Lines this PR adds are highlighted in green (
+); everything else is existing output shown for context.Text (
codexbar usage)JSON (
codexbar usage --format json --pretty){ "provider": "codex", "version": "0.6.0", "source": "openai-web", "usage": { "primary": { "usedPercent": 28, "windowMinutes": 300, "resetsAt": "2025-12-04T19:15:00Z" }, "secondary": { "usedPercent": 59, "windowMinutes": 10080, "resetsAt": "2025-12-05T17:00:00Z" }, "tertiary": null, "updatedAt": "2025-12-04T18:10:22Z" }, + "pace": { + "primary": { "stage": "ahead", "deltaPercent": 12, "expectedUsedPercent": 16, "willLastToReset": false, "etaSeconds": 9000, "summary": "12% in deficit | Expected 16% used | Projected empty in 2h 30m" }, + "secondary": { "stage": "slightlyBehind", "deltaPercent": -6, "expectedUsedPercent": 47, "willLastToReset": true, "summary": "6% in reserve | Expected 47% used | Lasts until reset" } + }, "credits": { "remaining": 112.4, "updatedAt": "2025-12-04T18:10:21Z" } }pace.primary= session window,pace.secondary= weekly window. A key is emitted only when the corresponding text Pace line would render; nil fields (e.g.etaSecondswhen it lasts to reset) are omitted, consistent with the existing payload.deltaPercent = used − expected, rounded to whole values (positive = ahead of pace / deficit, negative = reserve); the raw usage figure stays inusage.<window>.usedPercent.Notes
UsagePaceText.sessionPace. Weekly is a plain linear approximation in the CLI — unlike the menu'sUsageStore.weeklyPaceit does not applyweeklyProgressWorkDaysor Codex historical refinement, and uses a fixed provider allowlist. (Pre-existing for the text weekly line; documented onPaceKind.)Test
swift test --filter CLISnapshotTests— 35 passing (session-pace text + JSON pace, rounding, omission, on-track / lasts-until-reset, non-session primary gating).swiftlint --strictandswiftformat --lintclean.🤖 Generated with Claude Code