Skip to content
Open
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ The extension will automatically attempt to locate the `codexbar` binary in comm

Certain vendors have specific fields in Codexbar CLI, whose interpretation may not have been implemented so this is a great opportunity for you to implement support (if you want) and do a PR.

### API Keys (e.g. OpenRouter)

Some providers (OpenRouter with `--source api`) authenticate the CodexBar CLI via an environment variable, e.g. `OPENROUTER_API_KEY`, instead of a token cached on disk. Setting that variable in `~/.zshrc`, `~/.bashrc`, or similar is **not enough**: GNOME Shell is started by your login/display manager, not by an interactive shell, so it never sources your shell's dotfiles, and any command the extension runs (as a child process of GNOME Shell) inherits GNOME Shell's environment, not your terminal's.

To make the variable visible to the extension, add it to your systemd user environment instead:

```bash
mkdir -p ~/.config/environment.d
echo 'OPENROUTER_API_KEY=sk-or-v1-...' > ~/.config/environment.d/codexbar.conf
chmod 600 ~/.config/environment.d/codexbar.conf
```

Then log out and log back in. `environment.d` files are only read once, when your `systemd --user` manager starts — if it's still running from before you added the file (e.g. lingering is enabled: `loginctl show-user $USER | grep Linger`), a normal logout/login won't pick it up. In that case, apply it to the running instance once, then log out/in as usual:

```bash
systemctl --user import-environment OPENROUTER_API_KEY
```

You can confirm GNOME Shell actually has the variable with:

```bash
tr '\0' '\n' < /proc/$(pgrep -x gnome-shell)/environ | grep OPENROUTER_API_KEY
```

Without it, `codexbar --provider openrouter --source api` still returns valid JSON, but with a top-level `error` field and no `usage.details` — so the OpenRouter tab shows only a bare, empty tier instead of the Credits / API key / Spend history breakdown.

### Display Mode

You can choose how metrics are displayed:
Expand Down
119 changes: 111 additions & 8 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import Gio from "gi://Gio";
import GLib from "gi://GLib";
import St from "gi://St";
import Clutter from "gi://Clutter";
import Pango from "gi://Pango";
import {
Extension,
gettext as _,
} from "resource:///org/gnome/shell/extensions/extension.js";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import { calculateUsagePace, UsageApiClient } from "./usageApi.js";
import {
calculateUsagePace,
deriveCreditsPercent,
normalizeDetailSections,
UsageApiClient,
} from "./usageApi.js";
import { loadToken, nullTokenSchema } from "./secret.js";

function logDev(msg) {
Expand Down Expand Up @@ -121,6 +127,7 @@ export default class CodexBarExtension extends Extension {
"changed::display-mode", () => this._updateUI(),
"changed::show-logos", () => this._updateUI(),
"changed::show-pacing-info", () => this._updateUI(),
"changed::show-provider-details", () => this._updateUI(),
"changed::first-run", () => this._updateUI(),
"changed::dev-custom-output-enabled", () => this._onSettingsChanged(),
"changed::dev-custom-output-provider-name", () => this._onSettingsChanged(),
Expand Down Expand Up @@ -536,13 +543,25 @@ export default class CodexBarExtension extends Extension {
let tierCount = 0;

const activeData = this._providersData[this._activeProviderIndex];
// A tier with usedPercent:0 and no windowSeconds isn't a real usage window
// (e.g. OpenRouter's balance placeholder) - derive a meaningful percent from
// the Credits detail section instead, or drop the tier if none is available.
const creditsPercent = deriveCreditsPercent(
normalizeDetailSections(activeData?.data?.usage?.details),
);
const isDegenerateTier = (tierData) =>
!!tierData && !tierData.windowSeconds && tierData.usedPercent === 0;

if (activeData && activeData.data && activeData.data.usage) {
const usage = activeData.data.usage;
const tiers = ["primary", "secondary", "tertiary", "quaternary"];

tiers.forEach((tier) => {
if (usage[tier] && usage[tier].usedPercent !== undefined) {
let p = this._normalizePercent(usage[tier].usedPercent);
if (isDegenerateTier(usage[tier]) && creditsPercent === null) return;
let p = isDegenerateTier(usage[tier])
? creditsPercent
: this._normalizePercent(usage[tier].usedPercent);
totalPercent += displayMode === "remaining" ? 100 - p : p;
tierCount++;
}
Expand Down Expand Up @@ -742,19 +761,31 @@ export default class CodexBarExtension extends Extension {
this._contentBox.add_child(accountBox);
}

this._renderDetailSections(usage);

// Usage bars for each tier
// Barras de uso para cada nivel
const tiers = ["primary", "secondary", "tertiary", "quaternary"];
const discoveredLabels = activeData.labels || [];
let hasTiers = false;

const usageEntries = tiers.map((tier, tierIdx) => ({
data: usage[tier],
showPace: true,
title:
const usageEntries = tiers.map((tier, tierIdx) => {
let tierData = usage[tier];
let title =
discoveredLabels[tierIdx] ||
tier.charAt(0).toUpperCase() + tier.slice(1),
}));
tier.charAt(0).toUpperCase() + tier.slice(1);

if (isDegenerateTier(tierData)) {
if (creditsPercent === null) {
tierData = null;
} else {
tierData = { ...tierData, usedPercent: creditsPercent };
title = _("Credits");
}
}

return { data: tierData, showPace: true, title };
});
usageEntries.push({
data: usage.codeReview,
showPace: false,
Expand Down Expand Up @@ -936,6 +967,78 @@ export default class CodexBarExtension extends Extension {
}
}

/**
* Render provider-supplied detail sections (codexbar `usage.details`).
* @param {object} usage
*/
_renderDetailSections(usage) {
if (!this._settings.get_boolean("show-provider-details")) return;

const sections = normalizeDetailSections(usage?.details);
if (sections.length === 0) return;

const detailsBox = new St.BoxLayout({
vertical: true,
style_class: "codexbar-details-section",
x_expand: true,
});

sections.forEach((section) => {
const groupBox = new St.BoxLayout({
vertical: true,
style_class: "codexbar-detail-group",
x_expand: true,
});

if (section.title) {
groupBox.add_child(
new St.Label({
text: section.title,
style_class: "codexbar-detail-title",
}),
);
}

section.rows.forEach((row) => {
const rowBox = new St.BoxLayout({ vertical: false, x_expand: true });

const labelWidget = new St.Label({
text: row.label,
style_class: "codexbar-detail-label",
});
labelWidget.clutter_text.ellipsize = Pango.EllipsizeMode.END;
labelWidget.opacity = 200;
rowBox.add_child(labelWidget);

const valueWidget = new St.Label({
text: row.value,
style_class: "codexbar-detail-value",
x_align: Clutter.ActorAlign.END,
x_expand: true,
});
valueWidget.clutter_text.ellipsize = Pango.EllipsizeMode.END;
rowBox.add_child(valueWidget);

groupBox.add_child(rowBox);

if (row.secondaryValue) {
const noteWidget = new St.Label({
text: row.secondaryValue,
style_class: "codexbar-detail-note",
});
noteWidget.clutter_text.line_wrap = true;
noteWidget.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
noteWidget.opacity = 160;
groupBox.add_child(noteWidget);
}
});

detailsBox.add_child(groupBox);
});

this._contentBox.add_child(detailsBox);
}

/**
* Get provider logo as an St.Icon.
* Obtener el logo del proveedor como un St.Icon.
Expand Down
15 changes: 15 additions & 0 deletions prefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,21 @@ const CodexBarPrefsPage = GObject.registerClass(
);
group.add(showPacingInfoRow);

const showProviderDetailsRow = new Adw.SwitchRow({
title: _("Show Provider Details"),
subtitle: _(
"Display the extra detail sections reported by the provider CLI (credits, spend history, rate limits)",
),
active: this._settings.get_boolean("show-provider-details"),
});
this._settings.bind(
"show-provider-details",
showProviderDetailsRow,
"active",
Gio.SettingsBindFlags.DEFAULT,
);
group.add(showProviderDetailsRow);

return group;
}

Expand Down
4 changes: 4 additions & 0 deletions schemas/org.gnome.shell.extensions.codexbar.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
<default>true</default>
<summary>Whether to show weekly usage pacing details</summary>
</key>
<key name="show-provider-details" type="b">
<default>true</default>
<summary>Whether to show provider-supplied detail sections</summary>
</key>
<key name="dev-custom-output-enabled" type="b">
<default>false</default>
<summary>Enable developer custom output simulation</summary>
Expand Down
33 changes: 33 additions & 0 deletions stylesheet.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,39 @@
margin-top: 2px;
}

.codexbar-details-section {
margin-bottom: 8px;
}

.codexbar-detail-group {
margin-bottom: 10px;
}

.codexbar-detail-title {
font-size: 0.9em;
font-weight: bold;
margin-bottom: 4px;
}

.codexbar-detail-label {
font-size: 0.8em;
margin-bottom: 1px;
}

.codexbar-detail-value {
font-size: 0.8em;
font-weight: bold;
margin-bottom: 1px;
padding-left: 12px;
}

.codexbar-detail-note {
font-size: 0.75em;
font-style: italic;
margin-left: 10px;
margin-bottom: 3px;
}

.codexbar-footer {
padding: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
Expand Down
Loading