contents/ui/PanelDisplay.js

.pragma library
.import "ProviderNormalizer.js" as Normalizer

var percentMode = "percent";
var paceMode = "pace";
var bothMode = "both";
var resetTimeMode = "resetTime";
var runOutMode = "runOut";

function safeMode(value) {
    var mode = String(value || percentMode);
    if (mode === percentMode || mode === paceMode || mode === bothMode || mode === resetTimeMode || mode === runOutMode) {
        return mode;
    }
    return percentMode;
}

function safeLane(value) {
    return value === "primary" || value === "secondary" || value === "tertiary" ? value : "auto";
}

function hasFiniteNumber(value) {
    return typeof value === "number" && isFinite(value);
}

function rowHasPercent(row) {
    return row !== null && row !== undefined && row.hasPercent === true && hasFiniteNumber(row.usedPercent) && hasFiniteNumber(row.leftPercent);
}

function rowHasPace(row) {
    return row !== null && row !== undefined && Normalizer.authoritativeQuota(row) && hasFiniteNumber(row.pacePercent) && row.pacePercent >= 0;
}

function rowHasReset(row) {
    if (!row || !Normalizer.authoritativeQuota(row)) {
        return false;
    }
    return (typeof row.resetsAt === "string" && row.resetsAt.trim().length > 0) || (typeof row.resetDescription === "string" && row.resetDescription.trim().length > 0) || (typeof row.reset === "string" && row.reset.trim().length > 0);
}

function rowHasRunOut(row) {
    return row !== null && row !== undefined && Normalizer.authoritativeQuota(row) && row.paceOnTop === false && hasFiniteNumber(row.paceEtaSeconds) && row.paceEtaSeconds > 0;
}

function rowSupportsMode(row, value) {
    var mode = safeMode(value);
    switch (mode) {
    case paceMode:
        return rowHasPace(row);
    case bothMode:
        return rowHasPercent(row) || rowHasPace(row);
    case resetTimeMode:
        return rowHasReset(row);
    case runOutMode:
        return rowHasRunOut(row);
    default:
        return rowHasPercent(row);
    }
}

// Inputs are normalized usage rows and a canonical provider key. QML supplies
// the localized label; this module only constructs and orders quota candidates.
function candidateRows(rows, providerKey, providerCost, includedPlanLabel) {
    if (!rows || rows.length === 0) {
        return [];
    }
    var primary = rowForLane(rows, "primary");
    var secondary = rowForLane(rows, "secondary");
    var tertiary = rowForLane(rows, "tertiary");
    var preferred = [];
    if (providerKey === "factory") {
        appendUniqueRow(preferred, secondary);
        appendUniqueRow(preferred, primary);
    } else if (providerKey === "perplexity" && primary && primary.hasPercent
            && primary.leftPercent <= 0) {
        appendUniqueRow(preferred, secondary);
        appendUniqueRow(preferred, tertiary);
        appendUniqueRow(preferred, primary);
    } else {
        appendUniqueRow(preferred, primary);
        appendUniqueRow(preferred, secondary);
        appendUniqueRow(preferred, tertiary);
    }
    if (providerKey === "cursor" && primary && primary.hasPercent && primary.leftPercent <= 0
            && providerCost && providerCost.percentUsed >= 0) {
        var used = Math.max(0, Math.min(100, Number(providerCost.percentUsed)));
        preferred.unshift({
            lane: "providerCost",
            label: includedPlanLabel,
            hasPercent: true,
            usedPercent: used,
            leftPercent: Math.max(0, Math.min(100, 100 - used)),
            pacePercent: -1,
            paceOnTop: true,
            reset: "",
            pace: ""
        });
    }
    for (var i = 0; i < rows.length; i++) {
        appendUniqueRow(preferred, rows[i]);
    }
    return preferred;
}

function rowForLane(rows, lane) {
    for (var i = 0; i < rows.length; i++) {
        if (rows[i] && rows[i].lane === lane) {
            return rows[i];
        }
    }
    return null;
}

function appendUniqueRow(rows, row) {
    if (row && rows.indexOf(row) === -1) {
        rows.push(row);
    }
}

// The caller supplies provider-specific preference order. This function only
// decides whether a row carries the data required by the selected mode.
function rowForMode(rows, value, laneValue) {
    if (!Array.isArray(rows)) {
        return null;
    }
    var lane = safeLane(laneValue);
    if (lane !== "auto") {
        rows = rows.filter(function(row) { return row && row.lane === lane; });
    }
    var mode = safeMode(value);
    if (mode === bothMode) {
        for (var i = 0; i < rows.length; i++) {
            if (rowHasPercent(rows[i]) && rowHasPace(rows[i])) {
                return rows[i];
            }
        }
    }
    for (var j = 0; j < rows.length; j++) {
        if (rowSupportsMode(rows[j], mode)) {
            return rows[j];
        }
    }
    return null;
}

// Meter positions follow named lanes, not the provider's text preference or
// current consumption. Providers without those lanes keep their single fallback.
function meterRows(rows, laneValue) {
    var lane = safeLane(laneValue);
    if (lane !== "auto") {
        var selected = rowForMode(rows, percentMode, lane);
        return selected ? [selected] : [];
    }
    var result = [];
    var primary = rowForMode(rows, percentMode, "primary");
    var secondary = rowForMode(rows, percentMode, "secondary");
    if (primary) result.push(primary);
    if (secondary) result.push(secondary);
    if (result.length === 0) {
        var fallback = rowForMode(rows, percentMode);
        if (fallback) result.push(fallback);
    }
    return result;
}

// Forecast ETA is a duration observed with one usage snapshot, not an absolute
// timestamp. Advance it from that observation without triggering another CLI run.
function remainingSeconds(durationSeconds, observedAtMs, nowMs) {
    if (!hasFiniteNumber(durationSeconds) || durationSeconds < 0 || !hasFiniteNumber(observedAtMs) || !hasFiniteNumber(nowMs)) {
        return 0;
    }
    var elapsedSeconds = Math.max(0, (nowMs - observedAtMs) / 1000);
    return Math.max(0, durationSeconds - elapsedSeconds);
}