Skip to content
Merged
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ reload:
gnome-extensions disable codex-meter@slobbe.github.io
$(MAKE) install
gnome-extensions enable codex-meter@slobbe.github.io
$(MAKE) clean

clean:
@rm -f $(ZIP)
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Codex Meter

Monitor and manage Codex usage limits.

<div align="center">
<h1>Codex Meter</h1>
<p>Monitor and manage Codex usage limits.</p>
</div>
<p align="center">
<img src="docs/assets/screens/popup.png" width="325" alt="Codex Meter popup preview">
</p>
Expand Down
Binary file added docs/assets/screens/preferences.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-meter",
"version": "0.9.2",
"version": "0.9.3",
"description": "Monitor and manage Codex usage limits",
"type": "module",
"private": true,
Expand Down
56 changes: 48 additions & 8 deletions src/background/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
listCodexBankedResets,
readCachedCodexBankedResets,
redeemCodexBankedReset,
redeemNextCodexBankedReset,
} from "../codex/banked-resets/api.js";
import { selectCreditExpiringWithin } from "../codex/banked-resets/response.js";
import {
selectCreditExpiringWithin,
selectCreditToRedeem,
} from "../codex/banked-resets/response.js";
import { PeriodicTask } from "./periodic-task.js";
import { formatRefreshFailure } from "./refresh-error-message.js";

Expand Down Expand Up @@ -49,6 +51,8 @@ export class CodexMonitor {
this._lastBankedResetRefreshAt = null;
this._bankedResetRefreshPromise = null;
this._bankedResetRefreshCancellable = null;
this._preparingBankedReset = false;
this._prepareBankedResetCancellable = null;
this._redeemingBankedReset = false;
this._redeemBankedResetCancellable = null;
this._stopped = false;
Expand All @@ -63,6 +67,7 @@ export class CodexMonitor {
errorMessage: this._errorMessage,
cachedFailureMessage: this._cachedFailureMessage,
bankedResetCount: this._bankedResetCount,
preparingBankedReset: this._preparingBankedReset,
redeemingBankedReset: this._redeemingBankedReset,
};
}
Expand All @@ -85,6 +90,8 @@ export class CodexMonitor {
this._refreshCancellable = null;
this._bankedResetRefreshCancellable?.cancel();
this._bankedResetRefreshCancellable = null;
this._prepareBankedResetCancellable?.cancel();
this._prepareBankedResetCancellable = null;
this._redeemBankedResetCancellable?.cancel();
this._redeemBankedResetCancellable = null;
this._settings.disconnect(this._autoApplyChangedId);
Expand Down Expand Up @@ -126,18 +133,46 @@ export class CodexMonitor {
}
}

async redeemBankedReset() {
if (this._stopped || this._redeemingBankedReset) return;
if (this._bankedResetCount !== null && this._bankedResetCount <= 0) {
this._notify("No banked Codex resets are available to redeem.");
return;
async prepareBankedReset() {
if (this._stopped || this._preparingBankedReset || this._redeemingBankedReset) return null;
const cancellable = new Gio.Cancellable();
this._preparingBankedReset = true;
this._prepareBankedResetCancellable = cancellable;
this._emitChange();
try {
const response = await listCodexBankedResets({ cancellable });
if (this._stopped) return null;
this._bankedResetCount = Math.max(0, response.available_count);
this._lastBankedResetRefreshAt = Math.floor(Date.now() / 1000);
const credit = selectCreditToRedeem(response.credits);
if (!credit) {
this._bankedResetCount = 0;
this._notify("No banked Codex resets are available to redeem.");
}
return credit;
} catch (error) {
if (isCancellationError(error) || this._stopped) return null;
this._notify(formatBankedResetPreparationFailure(error));
return null;
} finally {
if (this._prepareBankedResetCancellable === cancellable) {
this._prepareBankedResetCancellable = null;
}
if (!this._stopped) {
this._preparingBankedReset = false;
this._emitChange();
}
}
}

async redeemBankedReset(creditId) {
if (this._stopped || this._redeemingBankedReset) return;
const cancellable = new Gio.Cancellable();
this._redeemingBankedReset = true;
this._redeemBankedResetCancellable = cancellable;
this._emitChange();
try {
await redeemNextCodexBankedReset({ cancellable });
await redeemCodexBankedReset(creditId, { cancellable });
if (this._stopped) return;
this._notify("Banked Codex reset redeemed.");
await this.refreshBankedResets(cancellable);
Expand Down Expand Up @@ -328,6 +363,11 @@ export class CodexMonitor {
}
}

function formatBankedResetPreparationFailure(error) {
if (isCodexError(error)) return error.message;
return "Unable to load banked Codex reset details. Try again later.";
}

function formatBankedResetFailure(error) {
if (isCodexError(error)) return error.message;
const message =
Expand Down
17 changes: 1 addition & 16 deletions src/codex/banked-resets/api.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import GLib from "gi://GLib";
import { fetchJson } from "../../io/http.js";
import { CodexError } from "../error.js";
import { readBankedResetSnapshot, writeBankedResetSnapshot } from "./store.js";
import { getCodexCredentials } from "../auth/auth.js";
import { selectCreditToRedeem, toListResponse } from "./response.js";
import { toListResponse } from "./response.js";

const CODEX_BANKED_RESETS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";

Expand Down Expand Up @@ -41,20 +40,6 @@ export async function readCachedCodexBankedResets() {
return await readBankedResetSnapshot();
}

export async function redeemNextCodexBankedReset(options = {}) {
const list = await listCodexBankedResets(options);
const credit = selectCreditToRedeem(list.credits);
if (!credit) {
throw new CodexError(
"unexpected-response",
"No banked Codex resets are available to redeem.",
"Codex banked reset redemption was requested with zero available credits.",
);
}
await redeemCodexBankedReset(credit.id, options);
return credit;
}

export async function redeemCodexBankedReset(creditId, options = {}) {
const response = await consumeCodexBankedReset(creditId, options);
try {
Expand Down
2 changes: 1 addition & 1 deletion src/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"settings-schema": "org.gnome.shell.extensions.codex-meter",
"url": "https://github.com/slobbe/codex-meter",
"license": "GPL-3.0-or-later",
"version-name": "0.9.2"
"version-name": "0.9.3"
}
31 changes: 28 additions & 3 deletions src/panel/indicator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import St from "gi://St";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
import { readSettings } from "../preferences/settings.js";
import { CodexMeterPopupMenu } from "./menu.js";
import { showBankedResetConfirmation } from "./reset-confirmation.js";
import { UsageBar } from "./usage-bar.js";
import { createMenuViewModel, createPanelBarViewModel } from "./view-model.js";

Expand All @@ -26,6 +27,7 @@ export class CodexMeterIndicator extends PanelMenu.Button {
};
this._refreshSpinId = 0;
this._menuSyncId = 0;
this._resetConfirmationDialog = null;
this._destroyed = false;
this._panelBox = new St.BoxLayout({
y_align: Clutter.ActorAlign.CENTER,
Expand Down Expand Up @@ -91,6 +93,8 @@ export class CodexMeterIndicator extends PanelMenu.Button {
this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = 0;
}
this._resetConfirmationDialog?.destroy();
this._resetConfirmationDialog = null;

super.destroy();
}
Expand All @@ -101,7 +105,7 @@ export class CodexMeterIndicator extends PanelMenu.Button {
void this._refreshUsage();
},
onRedeemBankedReset: () => {
void this._redeemBankedReset();
void this._confirmBankedReset();
},
onOpenPreferences: () => {
this.menu.close();
Expand Down Expand Up @@ -147,11 +151,30 @@ export class CodexMeterIndicator extends PanelMenu.Button {
}
}

async _redeemBankedReset() {
async _confirmBankedReset() {
if (this._destroyed || this._resetConfirmationDialog) return;
const credit = await this._monitor.prepareBankedReset();
if (this._destroyed || !credit) return;
this.menu.close();
let dialog = null;
const clearDialog = () => {
if (this._resetConfirmationDialog === dialog) this._resetConfirmationDialog = null;
};
dialog = showBankedResetConfirmation(credit, {
onConfirm: () => {
clearDialog();
void this._redeemBankedReset(credit.id);
},
onCancel: clearDialog,
});
this._resetConfirmationDialog = dialog;
}

async _redeemBankedReset(creditId) {
if (this._destroyed) return;
this._startRefreshSpin();
try {
await this._monitor.redeemBankedReset();
await this._monitor.redeemBankedReset(creditId);
} finally {
if (!this._destroyed) this._stopRefreshSpin();
}
Expand Down Expand Up @@ -220,6 +243,7 @@ export class CodexMeterIndicator extends PanelMenu.Button {
errorMessage,
cachedFailureMessage,
bankedResetCount,
preparingBankedReset,
redeemingBankedReset,
} = this._monitor.state;
const viewModel = createMenuViewModel(
Expand All @@ -241,6 +265,7 @@ export class CodexMeterIndicator extends PanelMenu.Button {
this._popupMenu.setTrend(viewModel.trend);
this._popupMenu.setBankedResets({
count: bankedResetCount,
preparing: preparingBankedReset,
redeeming: redeemingBankedReset,
});
this._popupMenu.footerItem.planLabel.text = viewModel.plan;
Expand Down
13 changes: 8 additions & 5 deletions src/panel/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,18 @@ export class CodexMeterPopupMenu {
this.statusItem.messageLabel.text = message ?? "";
}

setBankedResets({ count, redeeming }) {
const canRedeem = !redeeming && count !== null && count > 0;
setBankedResets({ count, preparing, redeeming }) {
const busy = preparing || redeeming;
const canRedeem = !busy && count !== null && count > 0;
this.headerItem.redeemButton.visible = count !== null;
this.headerItem.redeemButton.reactive = canRedeem;
this.headerItem.redeemButton.can_focus = canRedeem;
this.headerItem.redeemButton.opacity = canRedeem ? 255 : 150;
this.headerItem.redeemButtonLabel.text = redeeming
? "Resetting…"
: `Reset limits (${count ?? 0})`;
this.headerItem.redeemButtonLabel.text = preparing
? "Loading…"
: redeeming
? "Resetting…"
: `Reset limits (${count ?? 0})`;
}

_createUsageItem(title) {
Expand Down
Loading