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
48 changes: 36 additions & 12 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,35 @@ extension StatusItemController {
// interaction closures must always reference the live menu they end up serving.
let interactionMenu = captureMenu ?? menu
let providerScopes = self.overviewProviderScopes(enabledProviders: enabledProviders)
let rows: [(provider: UsageProvider, model: UsageMenuCardView.Model)] = providerScopes.visible
.compactMap { provider in
guard let model = self.menuCardModel(for: provider) else { return nil }
guard !model.isOverviewErrorOnly else { return nil }
return (provider: provider, model: model)
var rows: [(provider: UsageProvider, identifier: String, model: UsageMenuCardView.Model)] = []
for provider in providerScopes.visible {
if provider == .codex,
let display = self.codexAccountMenuDisplay(for: provider),
display.showAll
{
for account in display.accounts {
let accountSnapshot = display.snapshots.first(where: { $0.id == account.id })
let model = self.menuCardModel(
for: .codex,
snapshotOverride: accountSnapshot?.snapshot,
errorOverride: accountSnapshot?.error,
forceOverrideCard: accountSnapshot == nil,
accountOverride: self.accountInfo(for: account),
historySelectionOverride: self.store.codexPlanUtilizationHistorySelection(
forVisibleAccount: account),
creditsOverride: accountSnapshot?.credits)
guard let model, !model.isOverviewErrorOnly
else { continue }
rows.append((
provider: provider,
identifier: "\(Self.overviewRowIdentifierPrefix)codex-\(account.id)",
model: model))
}
continue
}
guard let model = self.menuCardModel(for: provider), !model.isOverviewErrorOnly else { continue }
rows.append((provider: provider, identifier: "\(Self.overviewRowIdentifierPrefix)\(provider.rawValue)", model: model))
}
guard !rows.isEmpty else { return false }

let t0 = CACurrentMediaTime()
Expand Down Expand Up @@ -622,17 +645,18 @@ extension StatusItemController {
}

for (index, row) in rows.enumerated() {
let identifier = "\(Self.overviewRowIdentifierPrefix)\(row.provider.rawValue)"
let storageText = self.store.storageFootprintText(for: row.provider)
let submenu = self.makeOverviewRowSubmenu(
provider: row.provider,
model: row.model,
width: menuWidth)
let submenu = row.identifier.contains("codex-")
? nil
: self.makeOverviewRowSubmenu(
provider: row.provider,
model: row.model,
width: menuWidth)
let item = self.makeMenuCardItem(
OverviewMenuCardRowView(model: row.model, storageText: storageText, width: menuWidth),
id: identifier,
id: row.identifier,
width: menuWidth,
heightCacheScope: row.provider.rawValue,
heightCacheScope: row.identifier,
heightCacheFingerprint: row.model.heightFingerprint(
section: "overview",
additional: [UsageMenuCardView.Model.heightFingerprintField("storage", storageText)]),
Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/StatusItemController+OverviewSubmenus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ extension StatusItemController {
return
}
let rawProvider = String(represented.dropFirst(Self.overviewRowIdentifierPrefix.count))
guard let provider = UsageProvider(rawValue: rawProvider),
let providerRawValue = rawProvider.hasPrefix("codex-") ? UsageProvider.codex.rawValue : rawProvider
guard let provider = UsageProvider(rawValue: providerRawValue),
let menu = sender.menu
else {
return
Expand Down
9 changes: 7 additions & 2 deletions Sources/CodexBarCLI/CLIPayloads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct ProviderPayload: Encodable {
let diagnostic: String?
let error: ProviderErrorPayload?
let pace: ProviderPacePayload?
let accountActive: Bool?

private enum CodingKeys: String, CodingKey {
case provider
Expand Down Expand Up @@ -47,7 +48,8 @@ struct ProviderPayload: Encodable {
openaiDashboard: OpenAIDashboardSnapshot?,
error: ProviderErrorPayload?,
diagnostic: String? = nil,
pace: ProviderPacePayload? = nil)
pace: ProviderPacePayload? = nil,
accountActive: Bool? = nil)
{
self.provider = provider.rawValue
self.account = account
Expand All @@ -62,6 +64,7 @@ struct ProviderPayload: Encodable {
self.diagnostic = diagnostic
self.error = error
self.pace = pace
self.accountActive = accountActive
}

init(
Expand All @@ -77,7 +80,8 @@ struct ProviderPayload: Encodable {
openaiDashboard: OpenAIDashboardSnapshot?,
error: ProviderErrorPayload?,
diagnostic: String? = nil,
pace: ProviderPacePayload? = nil)
pace: ProviderPacePayload? = nil,
accountActive: Bool? = nil)
{
self.provider = providerID
self.account = account
Expand All @@ -92,6 +96,7 @@ struct ProviderPayload: Encodable {
self.diagnostic = diagnostic
self.error = error
self.pace = pace
self.accountActive = accountActive
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCLI/CLIServeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,7 @@ extension CodexBarCLI {
providerTimeout: providerTimeout,
providerDeadline: providerDeadline,
providerOperations: runtime.providerOperations,
includeAllCodexAccounts: false),
includeAllCodexAccounts: true),
costCollection: ServeCostCollectionContext(
configFingerprint: snapshot.cacheToken,
providerTimeout: providerTimeout,
Expand Down
150 changes: 142 additions & 8 deletions Sources/CodexBarCLI/CLIServeWebUI+HTML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,42 @@ extension CLIServeWebUI {
margin-bottom: 22px;
}

.provider-group-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}

.dashboard-settings {
position: relative;
}

.dashboard-settings summary {
cursor: pointer;
}

.dashboard-settings-panel {
position: absolute;
right: 0;
top: calc(100% + 8px);
z-index: 5;
width: 250px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--surface);
box-shadow: var(--shadow);
}

.dashboard-settings-panel label {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
color: var(--text);
}

.brand {
gap: 10px;
min-width: 0;
Expand Down Expand Up @@ -172,6 +208,13 @@ extension CLIServeWebUI {
font-size: 12px;
}

.settings-button {
display: inline-block;
padding: 3px 8px;
color: var(--muted);
font-size: 12px;
}

.sign-out.visible {
display: inline-block;
}
Expand Down Expand Up @@ -609,6 +652,9 @@ extension CLIServeWebUI {

const tokenKey = "codexbar.dashboardToken";
const snapshotKey = "codexbar.lastSnapshot";
const preferenceKey = "codexbar.dashboardPreferences";
const defaultPreferences = { hideSpark: false, showResetCredits: true };
let preferences = loadPreferences();
const providerIconURLs = __PROVIDER_ICON_URLS__;
const state = {
snapshot: null,
Expand Down Expand Up @@ -636,6 +682,52 @@ extension CLIServeWebUI {
version: document.getElementById("version")
};

function loadPreferences() {
try {
const saved = JSON.parse(localStorage.getItem(preferenceKey) || "{}");
// Earlier versions stored these Codex-only choices at the top level.
const codex = saved?.providers?.codex || saved || {};
return { providers: { codex: {
hideSpark: typeof codex.hideSpark === "boolean" ? codex.hideSpark : defaultPreferences.hideSpark,
showResetCredits: typeof codex.showResetCredits === "boolean"
? codex.showResetCredits : defaultPreferences.showResetCredits
} } };
} catch (_) {
return { providers: { codex: { ...defaultPreferences } } };
}
}

function displayPreferences(providerID) {
return preferences.providers[providerID] || { hideSpark: false, showResetCredits: false };
}

function renderDisplaySettings(provider) {
// Only Codex currently has configurable display items.
if (provider.id !== "codex") return null;
const details = node("details", "dashboard-settings");
const summary = node("summary", "settings-button", "Display");
summary.setAttribute("aria-label", `${provider.name || provider.id} display settings`);
details.append(summary);
const panel = node("div", "dashboard-settings-panel");
panel.append(node("strong", "", `${provider.name || provider.id} display`));
for (const [key, text] of [["hideSpark", "Hide Spark usage"],
["showResetCredits", "Show Limit Reset count"]]) {
const label = node("label");
const input = node("input");
input.type = "checkbox";
input.checked = displayPreferences(provider.id)[key];
input.addEventListener("change", () => {
preferences.providers[provider.id][key] = input.checked;
try { localStorage.setItem(preferenceKey, JSON.stringify(preferences)); } catch (_) {}
if (state.snapshot) renderSnapshot(state.snapshot, state.forceStale);
});
label.append(input, node("span", "", text));
panel.append(label);
}
details.append(panel);
return details;
}

function storedToken() {
try {
return localStorage.getItem(tokenKey) || "";
Expand Down Expand Up @@ -852,11 +944,12 @@ extension CLIServeWebUI {
return dot;
}

function visibleWindows(windows) {
function visibleWindows(windows, providerID) {
// The producer marks a window idle when its whole model family reports no usage, which
// the page cannot work out on its own: a zero percentage also stands for a lane the
// provider never reported. Script clients still receive every window on the snapshot.
return (windows || []).filter(w => w.idle !== true);
return (windows || []).filter(w => w.idle !== true &&
!(displayPreferences(providerID).hideSpark && (w.kind === "codex-spark" || w.kind === "codex-spark-weekly")));
}

function worstWindowLevel(windows) {
Expand Down Expand Up @@ -888,14 +981,15 @@ extension CLIServeWebUI {
if (account.active) {
head.append(pill("active", "active"));
} else {
const level = worstWindowLevel(visibleWindows(account.windows));
const level = worstWindowLevel(visibleWindows(account.windows, provider.id));
if (level) head.append(pill(level, level === "ok" ? "ok" : level === "warning" ? "high" : "critical"));
}
card.append(head);

if (account.updatedAt) {
if (account.updatedAt || account.identity?.plan) {
const identity = node("div", "identity");
identity.append(node("span", "", `updated ${relativeTime(account.updatedAt)}`));
if (account.identity?.plan) identity.append(node("span", "", account.identity.plan));
if (account.updatedAt) identity.append(node("span", "", `updated ${relativeTime(account.updatedAt)}`));
card.append(identity);
}

Expand All @@ -906,8 +1000,26 @@ extension CLIServeWebUI {
}

const windows = node("div", "windows");
for (const window of visibleWindows(account.windows)) windows.append(renderWindow(window));
for (const window of visibleWindows(account.windows, provider.id)) windows.append(renderWindow(window));
card.append(windows);
if (account.credits?.remaining !== null && account.credits?.remaining !== undefined) {
const credits = node("div", "metrics");
const unit = account.credits.unit ? ` ${account.credits.unit}` : "";
credits.append(metric("Remaining", `${amount(account.credits.remaining)}${unit}`));
card.append(credits);
}
if (displayPreferences(provider.id).showResetCredits && account.resetCreditsAvailable !== null &&
account.resetCreditsAvailable !== undefined) {
const metrics = node("div", "metrics");
metrics.append(metric("Limit resets", amount(account.resetCreditsAvailable)));
card.append(metrics);
const nextReset = (account.resetCredits || [])
.filter(reset => reset && reset.expiresAt)
.sort((left, right) => dateValue(left.expiresAt) - dateValue(right.expiresAt))[0];
if (nextReset) {
metrics.append(metric("Earliest expiry", relativeTime(nextReset.expiresAt)));
}
}
return card;
}

Expand Down Expand Up @@ -937,6 +1049,8 @@ extension CLIServeWebUI {
status.append(dot, node("span", "status-label", statusLabel));
head.append(status);
}
const settings = renderDisplaySettings(provider);
if (settings) head.append(settings);
card.append(head);

if (provider._pending) {
Expand All @@ -960,7 +1074,7 @@ extension CLIServeWebUI {
}

const windows = node("div", "windows");
for (const window of visibleWindows(provider.windows)) windows.append(renderWindow(window));
for (const window of visibleWindows(provider.windows, provider.id)) windows.append(renderWindow(window));
card.append(windows);
if (provider.accountsError) {
card.append(node("p", "error-message", provider.accountsError));
Expand All @@ -971,6 +1085,10 @@ extension CLIServeWebUI {
const unit = provider.credits.unit ? ` ${provider.credits.unit}` : "";
metrics.append(metric("Remaining", `${amount(provider.credits.remaining)}${unit}`));
}
if (displayPreferences(provider.id).showResetCredits && provider.credits?.resetCreditsAvailable !== null &&
provider.credits?.resetCreditsAvailable !== undefined) {
metrics.append(metric("Limit resets", amount(provider.credits.resetCreditsAvailable)));
}
if (provider.cost?.todayUSD !== null && provider.cost?.todayUSD !== undefined) {
metrics.append(metric("Today", dollars(provider.cost.todayUSD)));
}
Expand Down Expand Up @@ -1024,7 +1142,23 @@ extension CLIServeWebUI {
const accounts = Array.isArray(provider.accounts) ? provider.accounts : [];
if (accounts.length) {
const group = node("section", "group");
group.append(node("h2", "group-title", `${provider.name || provider.id} accounts`));
const heading = node("div", "provider-group-heading");
heading.append(node("h2", "group-title", `${provider.name || provider.id} accounts`));
const settings = renderDisplaySettings(provider);
if (settings) heading.append(settings);
group.append(heading);
const summary = node("div", "metrics");
if (provider.credits?.remaining !== null && provider.credits?.remaining !== undefined) {
const unit = provider.credits.unit ? ` ${provider.credits.unit}` : "";
summary.append(metric("Remaining", `${amount(provider.credits.remaining)}${unit}`));
}
if (provider.cost?.todayUSD !== null && provider.cost?.todayUSD !== undefined) {
summary.append(metric("Today", dollars(provider.cost.todayUSD)));
}
if (provider.cost?.last30DaysUSD !== null && provider.cost?.last30DaysUSD !== undefined) {
summary.append(metric("Last 30 days", dollars(provider.cost.last30DaysUSD)));
}
if (summary.childNodes.length) group.append(summary);
const grid = node("div", "grid");
for (const account of accounts) grid.append(renderAccountCard(provider, account));
if (provider.accountsError) grid.append(node("p", "error-message", provider.accountsError));
Expand Down
Loading