Skip to content

Add Cursor period meters to model-usage - #6591

Open
rlimberger wants to merge 13 commits into
basecamp:quattrofrom
rlimberger:feat/model-usage-cursor
Open

Add Cursor period meters to model-usage#6591
rlimberger wants to merge 13 commits into
basecamp:quattrofrom
rlimberger:feat/model-usage-cursor

Conversation

@rlimberger

@rlimberger rlimberger commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Test plan

  • bash test/shell.d/model-usage-cursor-scanner-test.sh — missing DB, missing token, read-only plan mapping, mocked GetCurrentPeriodUsage success, HTTP 401 auth failure
  • bash test/shell.d/model-usage-claude-scanner-test.sh
  • bash test/shell.d/model-usage-codex-scanner-test.sh
  • Manual: signed-in Cursor shows Cursor Models / Other Models meters + tier; charts stay hidden (panel)
  • Manual: signed-out / missing Cursor stays self-hidden like Claude/Codex
  • Manual: Claude session vs weekly still shows per-row resets (no shared LIMITS hoist)
  • Manual: Cursor billing-cycle end appears once in the LIMITS header

Surface Cursor Models / Other Models from state.vscdb + GetCurrentPeriodUsage,
with panel icons, shared LIMITS reset, and hidden empty charts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI balanced review requested due to automatic review settings August 6, 2026 20:11
Cover missing token, mocked GetCurrentPeriodUsage success, and HTTP 401
auth failure alongside the existing missing-DB and plan-mapping cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
@rlimberger

rlimberger commented Aug 6, 2026

Copy link
Copy Markdown
Author

Signed-in Cursor tab (Ultra) showing Cursor Models + Other Models meters and shared LIMITS "Resets in …"; charts hidden; theme Retro 82 (dark). Panel-only crop (no IDE window).

Cursor model usage panel

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Cursor subscription meters to the model-usage plugin using Cursor’s local credentials and usage API.

Changes:

  • Adds Cursor scanning, provider integration, branding, and tests.
  • Adds shared reset headers and hides unavailable charts.
  • Documents and enables Cursor by default.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/shell.d/model-usage-cursor-scanner-test.sh Tests Cursor scanning and errors.
shell/plugins/model-usage/scripts/cursor_usage_scanner.py Reads credentials and fetches usage.
shell/plugins/model-usage/README.md Documents Cursor support.
shell/plugins/model-usage/providers/Cursor.qml Implements the Cursor provider.
shell/plugins/model-usage/Panel.qml Adds Cursor UI behavior and reset handling.
shell/plugins/model-usage/manifest.json Enables and describes Cursor.
shell/plugins/model-usage/Main.qml Registers Cursor with aggregation.
shell/plugins/model-usage/assets/cursor.svg Adds dark-background branding.
shell/plugins/model-usage/assets/cursor-light.svg Adds light-background branding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +158 to +160
readonly property bool hasDayChart: !!provider
&& Array.isArray(provider.recentDays)
&& provider.recentDays.length > 0
Gate day/model chart sections on hasLocalStats, and report false from
the Cursor scanner/provider so aggregateSnapshots' synthetic recentDays
rows do not show an all-zero TOKENS BY DAY chart.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 21:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (4)

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:78

  • The comment claims mode=ro prevents any writes including WAL sidecars; however, SQLite can still create/require WAL-related sidecar files (e.g., -shm) depending on the DB’s journal mode and existing WAL state. If the goal is to guarantee zero filesystem writes, consider using SQLite’s immutable=1 URI parameter (or adjust the comment to avoid overstating the guarantee).
    # Read-only URI so we never write, including WAL sidecars.
    uri = state_db.resolve().as_uri() + "?mode=ro"
    conn = sqlite3.connect(uri, uri=True, timeout=2)

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:140

  • When billingCycleEnd can’t be parsed, this returns the raw input string, which may not be ISO-8601. Downstream QML date parsing can then produce an invalid date and break/blank the reset countdown. Prefer returning an empty string (or a known-safe ISO string) when parsing fails, and rely on usageStatusText/authHelpText for diagnostics.
def parse_billing_cycle_end(value):
  ms = to_epoch_ms(value)
  if ms is None:
    text = str(value or "").strip()
    return text
  return datetime.fromtimestamp(ms / 1000.0, timezone.utc).isoformat()

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:115

  • The two thresholds (> 1e12 and > 1e10) currently do the same thing, which makes the intent unclear. Consider collapsing to a single threshold (with a brief comment) to distinguish ms vs seconds and avoid confusion for future maintenance.
  if isinstance(value, (int, float)):
    ms = float(value)
    if ms > 1e12:
      return int(ms)
    if ms > 1e10:
      return int(ms)
    return int(ms * 1000.0)

shell/plugins/model-usage/Panel.qml:510

  • The limitsHeaderReset id is very close to the root.limitsHeaderReset property name, which makes it easy to misread or accidentally reference the wrong one when editing. Renaming the Text id to something like limitsHeaderResetText would reduce ambiguity.
              Text {
                id: limitsHeaderReset
                visible: text !== ""
                text: root.limitsHeaderReset

Keep SQLite open mode=ro with an accurate WAL note, collapse epoch ms/seconds
detection, clear unparseable billingCycleEnd values, and rename the LIMITS
header Text id so it no longer shadows the property.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 03:28
Assert millisecond vs second detection and that unparseable
billingCycleEnd values become an empty resetAt string.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (1)

shell/plugins/model-usage/providers/Cursor.qml:88

  • This fixed five-minute timer bypasses the plugin's refreshIntervalSec setting and runs in addition to the scheduler in Main.qml:104-110. Consequently Cursor makes extra API requests (and toggles the widget's refreshing state) every five minutes even when the user configured a longer interval. The initial/enabled refresh is already covered by onEnabledChanged, so this duplicate timer should be removed or driven exclusively by the central scheduler.
  Timer {
    interval: 5 * 60 * 1000
    running: root.enabled
    repeat: true
    triggeredOnStart: true
    onTriggered: root.refresh()
  }

Defer to Main.qml's refreshIntervalSec scheduler so Cursor does not
hit GetCurrentPeriodUsage every five minutes on top of the plugin poll.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 03:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 9 changed files in this pull request and generated no new comments.

Automate signed-out hide, Claude vs Cursor LIMITS reset hoist,
shared billing-cycle header text, and syncMode chart suppression.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 03:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

shell/plugins/model-usage/Panel.qml:162

  • This still renders an empty seven-row day chart whenever recentDays is a synthesized array of zero-value rows. aggregateSnapshots() always creates seven such rows, and the Codex scanner does the same even without recent usage, so providers with only limits or older activity contradict this PR's empty-chart hiding behavior. Determine visibility from actual positive day totals (and update the test that currently expects zero rows to count as history), rather than array length alone.
  readonly property bool hasDayChart: !!provider
    && provider.hasLocalStats !== false
    && Array.isArray(provider.recentDays)
    && provider.recentDays.length > 0

Require a positive weekPeak for TOKENS BY DAY so syncMode and Codex
seven-row zero paddings do not count as chart history.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 03:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:214

  • json.loads() can successfully return null, an array, or another non-object value. In that case this .get() raises AttributeError, so the scanner emits no JSON and the panel retains stale limits instead of showing the intended “limits unavailable” state. Validate that payload is a dictionary before reading planUsage.
def build_rate_limits(credentials, payload):
  plan = payload.get("planUsage")

shell/plugins/model-usage/providers/Cursor.qml:45

  • Runtime QML paths must use the session-provided OMARCHY_PATH; resolving a component-relative file URL bypasses that canonical checkout path. Build the scanner path from Quickshell.env("OMARCHY_PATH") as required for Omarchy runtime code.
  readonly property string scannerPath: String(Qt.resolvedUrl("../scripts/cursor_usage_scanner.py")).replace("file://", "")

Reject non-object GetCurrentPeriodUsage payloads so the panel shows
limits unavailable instead of crashing, and resolve the scanner script
through OMARCHY_PATH.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 03:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (1)

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:142

  • An out-of-range numeric billingCycleEnd (for example, a timestamp in unexpected microseconds) passes to_epoch_ms() but makes datetime.fromtimestamp() raise ValueError, so malformed API data crashes the scanner instead of following this function's stated empty-reset fallback. Catch conversion/range errors around both steps so the provider can still return valid JSON.
  ms = to_epoch_ms(value)
  if ms is None:
    # Keep resetAt empty on unparseable input so QML date parsing stays valid.
    return ""
  return datetime.fromtimestamp(ms / 1000.0, timezone.utc).isoformat()

Copilot AI review requested due to automatic review settings August 7, 2026 03:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (1)

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:142

  • A numeric but non-finite or out-of-range billingCycleEnd can raise in to_epoch_ms() or datetime.fromtimestamp() (for example, JSON 1e309 becomes infinity). That terminates the scanner without JSON, so the provider can retain stale meters. Treat these conversion failures like other unparseable reset values.
def parse_billing_cycle_end(value):
  ms = to_epoch_ms(value)
  if ms is None:
    # Keep resetAt empty on unparseable input so QML date parsing stays valid.
    return ""
  try:

Copilot AI review requested due to automatic review settings August 7, 2026 04:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.github/instructions/bash.instructions.md:5

  • This repository-wide Bash review policy is unrelated to the Cursor provider and panel work described by this PR, and it changes future automated-review behavior without being disclosed in the summary. Please move this developer-tooling policy to a separate, documented PR so the feature change remains atomic.
# Omarchy bash review rules

Copilot AI review requested due to automatic review settings August 7, 2026 04:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:165

  • Reject non-finite percentages here. float("NaN") and float("Infinity") succeed, after which json.dumps emits bare NaN/Infinity; QML's strict JSON.parse then rejects the scanner's entire result instead of showing an unavailable meter. Validate with math.isfinite before returning the fraction.
def percent_to_fraction(value):
  if value is None:
    return -1
  try:
    return float(value) / 100.0
  except (TypeError, ValueError):
    return -1

Copilot AI review requested due to automatic review settings August 7, 2026 04:06
cursoragent and others added 2 commits August 7, 2026 04:07
Catch fromtimestamp OverflowError/OSError/ValueError so malformed
numeric API dates return an empty resetAt instead of crashing.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Treat inf/NaN and other conversion failures like unparseable reset
dates so the scanner still emits JSON instead of crashing.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
@cursor
cursor Bot force-pushed the feat/model-usage-cursor branch from 36ad860 to 08ddf40 Compare August 7, 2026 04:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 7, 2026 04:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

shell/plugins/model-usage/scripts/cursor_usage_scanner.py:165

  • Reject non-finite percentage values before serializing the scanner result. float("NaN"), float("Infinity"), and sufficiently large JSON numbers are accepted here, after which json.dumps emits NaN/Infinity; those are not valid JSON for JSON.parse in Cursor.qml, so one malformed API value makes the entire refresh fail instead of leaving that meter unset.
def percent_to_fraction(value):
  if value is None:
    return -1
  try:
    return float(value) / 100.0
  except (TypeError, ValueError):
    return -1

Keep NaN/Infinity out of scanner JSON so QML JSON.parse does not
fail the whole refresh when the API returns malformed percents.

Co-authored-by: Rene Limberger <rlimberger@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 04:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Keep Cursor meters-only via hasLocalStats; do not require a positive
weekPeak, which hid Codex's synthesized seven-day rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings August 7, 2026 09:04
@rlimberger

Copy link
Copy Markdown
Author

Restored Claude/Codex TOKENS BY DAY visibility for all-zero weeks (Codex synthesizes seven day rows). Cursor charts stay hidden via hasLocalStats: false — no weekPeak > 0 gate on Claude/Codex anymore.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants