contents/ui/main.qml
import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.plasmoid
import "components" as Components
import "controllers" as Controllers
import "AiInsights.js" as AiInsights
import "AiInsightsSnapshot.js" as AiInsightsSnapshot
import "Guards.js" as Guards
import "NotificationMemo.js" as NotificationMemo
import "NotificationPlanner.js" as NotificationPlanner
import "PacePresentation.js" as PacePresentation
import "PanelDisplay.js" as PanelDisplay
import "PanelElements.js" as PanelElements
import "PanelProviders.js" as PanelProviders
import "PanelRules.js" as PanelRules
import "PopupHiddenRows.js" as PopupHiddenRows
import "PopupHiddenSections.js" as PopupHiddenSections
import "PopupSelection.js" as PopupSelection
import "ProviderAutoSelect.js" as ProviderAutoSelect
import "ProviderSnapshot.js" as ProviderSnapshot
import "CostPresentation.js" as CostPresentation
import "ShareUsage.js" as ShareUsage
import "OverviewProviders.js" as OverviewProviders
import "ProviderIdentity.js" as ProviderIdentity
import "PrivacyPresentation.js" as PrivacyPresentation
import "ProviderNormalizer.js" as Normalizer
import "ProviderOrder.js" as ProviderOrder
import "UsageCache.js" as UsageCache
import "UsageState.js" as UsageState
import "ProviderViewModel.js" as ProviderViewModel
import "QuotaThresholds.js" as QuotaThresholds
import "SafeText.js" as SafeText
import "ThemeContrast.js" as ThemeContrast
import "UpdateLogic.js" as UpdateLogic
PlasmoidItem {
id: root
Plasmoid.icon: "view-statistics"
Plasmoid.title: "CodexBar"
toolTipMainText: Plasmoid.title
toolTipSubText: panelToolTipText()
toolTipTextFormat: Text.PlainText
Plasmoid.contextualActions: [
PlasmaCore.Action {
text: i18n("Refresh")
icon.name: "view-refresh"
onTriggered: root.refreshNow(true)
}
]
property string commandPath: (Plasmoid.configuration.commandPath || "codexbar").trim()
property string provider: (Plasmoid.configuration.provider || "").trim()
property string source: (Plasmoid.configuration.source || "").trim()
property int refreshIntervalSec: isFinite(Number(Plasmoid.configuration.refreshInterval)) ? Math.max(0, Number(Plasmoid.configuration.refreshInterval)) : 300
property bool includeStatus: Plasmoid.configuration.includeStatus
property bool refreshOnOpen: Plasmoid.configuration.refreshOnOpen === true
property bool showPopupPace: Plasmoid.configuration.showPopupPace !== false
property bool showPopupCredits: Plasmoid.configuration.showPopupCredits !== false
property bool showPopupProviderDetails: Plasmoid.configuration.showPopupProviderDetails !== false
property bool privacyMode: Plasmoid.configuration.privacyMode === true
readonly property bool aiInsightsEnabled: Plasmoid.configuration.aiInsightsEnabled === true
// Translators set this marker to their catalog's language tag, so insights
// follow the catalog i18n() actually resolved rather than the numeric or
// regional locale, which can differ from the interface language.
readonly property string aiInsightsLanguage: aiInsightsLanguageTag()
// Built only while enabled; disabled installations do no AI work at all.
readonly property var aiInsightsSnapshot: aiInsightsEnabled
? AiInsightsSnapshot.build(providers, panelClockMs, quotaWarningPercent)
: ({ text: "", id: "", sufficient: false })
readonly property var aiInsightsCache: aiInsightsEnabled
? AiInsights.parseCache(Plasmoid.configuration.aiInsightsCache || "") : null
readonly property string aiInsightsCacheState: AiInsights.cacheState(aiInsightsCache,
aiInsightsController.contextKey, panelClockMs, aiInsightsController.intervalHours)
readonly property bool aiInsightsConfigured: aiInsightsController.configured
readonly property bool aiInsightsBusy: aiInsightsController.busy
readonly property string aiInsightsErrorReason: aiInsightsController.errorReason
readonly property var presentedProviders: providerPresentations(providers)
readonly property var presentedOverviewProviders: providerPresentations(overviewProviderItems)
readonly property var presentedSessions: sessions.map(function(item) {
return PrivacyPresentation.session(item, privacyMode)
})
readonly property var presentedProviderData: providerPresentation(selectedProviderData)
property bool costUsageEnabled: Plasmoid.configuration.costUsageEnabled !== false
property int costHistoryDays: isFinite(Number(Plasmoid.configuration.costHistoryDays)) ? Math.max(1, Math.min(365, Number(Plasmoid.configuration.costHistoryDays))) : 30
// A calendar period replaces the day window until a day count is chosen
// again, here or in the settings.
readonly property string costHistoryPeriod: CostPresentation.costPeriod(Plasmoid.configuration.costHistoryPeriod)
// The cost payload already carries per-day tokens next to per-day cost, so
// switching the plotted metric never needs a second CLI call.
property string costHistoryMetric: safeCostHistoryMetric(Plasmoid.configuration.costHistoryMetric)
readonly property bool costHistoryShowsTokens: costHistoryMetric === "tokens"
// Resolved once: a .pragma library module cannot reach Qt.locale().
readonly property var costNumberFormat: CostPresentation.numberFormat(
Qt.locale().groupSeparator, Qt.locale().decimalPoint)
property bool usageBarsShowUsed: Plasmoid.configuration.usageBarsShowUsed !== false
property bool showQuotaWarningMarkers: Plasmoid.configuration.showQuotaWarningMarkers !== false
readonly property int quotaWarningPercent: QuotaThresholds.warningPercent(
Plasmoid.configuration.quotaWarningPercent)
// Derived from the warning step so a critical threshold configured below it
// can never make the "major" level unreachable.
readonly property int quotaCriticalPercent: QuotaThresholds.criticalPercent(
quotaWarningPercent,
Plasmoid.configuration.quotaCriticalPercent)
property bool enableNotifications: Plasmoid.configuration.enableNotifications !== false
property bool notifyStatusIncidents: Plasmoid.configuration.notifyStatusIncidents !== false
property bool notifyQuotaWarnings: Plasmoid.configuration.notifyQuotaWarnings !== false
property bool notifyPredictivePaceWarnings: Plasmoid.configuration.notifyPredictivePaceWarnings === true
property bool notifyLimitResets: Plasmoid.configuration.notifyLimitResets === true
property string menuBarDisplayMode: safeMenuBarDisplayMode(Plasmoid.configuration.menuBarDisplayMode)
property bool showPopupTabLabels: Plasmoid.configuration.showPopupTabLabels !== false
property string providerOrderRaw: Plasmoid.configuration.providerOrder || ""
property string panelProviderIDsRaw: Plasmoid.configuration.panelProviderIDs || ""
property string panelElementOrderRaw: Plasmoid.configuration.panelElementOrder || ""
readonly property bool minimalPanel: Plasmoid.configuration.panelStyle === "minimal"
readonly property string panelQuotaLane: PanelDisplay.safeLane(Plasmoid.configuration.panelQuotaLane)
readonly property var panelVisibilityRules: PanelRules.normalizedRules(Plasmoid.configuration.panelVisibilityRules)
property bool resetTimesShowAbsolute: Plasmoid.configuration.resetTimesShowAbsolute === true
property bool showProviderChangelogs: Plasmoid.configuration.showProviderChangelogs === true
property bool autoSelectProvider: Plasmoid.configuration.autoSelectProvider === true
property string overviewProviderIDsRaw: Plasmoid.configuration.overviewProviderIDs || ""
readonly property var popupHiddenUsageRows: PopupHiddenRows.parse(Plasmoid.configuration.popupHiddenUsageRows || "")
readonly property var popupHiddenDetailSections: PopupHiddenSections.parse(Plasmoid.configuration.popupHiddenDetailSections || "")
property int providerConfigRevision: boundedConfigRevision(Plasmoid.configuration.providerConfigRevision)
property var providers: []
readonly property var providerDisplayNames: usageController.providerDisplayNames
readonly property string errorText: usageController.errorText
readonly property bool commandPathFailed: usageController.commandPathFailed
property string lastUpdatedText: ""
property bool usageLifecycleInitialized: false
readonly property string usageIdentityContext: JSON.stringify([
commandPath, provider, source, providerConfigRevision])
readonly property bool loading: usageController.loading
// Reset labels and run-out durations keep moving even when automatic CLI
// refresh is disabled. Each row's receipt time anchors forecast durations;
// unrelated refreshes must not restart that countdown.
property double panelClockMs: Date.now()
readonly property int panelClockIntervalMs: 60000
readonly property string commandSource: usageController.commandSource
property string providerConfigStamp: ""
readonly property int maximumCostHistoryPoints: Normalizer.maximumCostHistoryPoints
readonly property bool costLoading: costController.loading
readonly property bool identifiedTokensEnabled: Plasmoid.configuration.identifiedTokensEnabled === true
readonly property bool identifiedTokensLoading: identifiedTokensController.loading
readonly property bool identifiedTokensSshAvailable: identifiedTokensController.sshAvailable
readonly property int identifiedTokensHistoryDays: identifiedTokensController.historyDays
readonly property var identifiedTokensSnapshot: identifiedTokensController.snapshot
readonly property string identifiedTokensErrorKind: identifiedTokensController.errorKind
function loadIdentifiedTokens(includeRemote) {
return identifiedTokensController.request(includeRemote === true)
}
readonly property var tokenCosts: presentTokenCosts(costController.costs)
property var costTrustNoticeStates: ({})
readonly property string costErrorText: costController.errorText
readonly property var sessions: sessionsController.sessions
readonly property string sessionsErrorText: sessionsController.errorText
readonly property string sessionsLastUpdatedText: sessionsController.lastUpdatedAtMs >= 0
? i18n("Updated %1", timeLabels.clockTime(sessionsController.lastUpdatedAtMs)) : ""
readonly property bool sessionsLoading: sessionsController.loading
property string selectedProviderID: ""
// Provider meter currently under the panel pointer. The plasmoid tooltip
// narrows to this provider while it is set, so hovering one panel icon
// reports only that provider instead of the whole roster.
property string hoveredPanelProviderID: ""
property string selectedGlobalView: "overview"
property bool selectionInitialized: false
property var selectedAccounts: ({})
readonly property var accountOptions: presentAccountOptions(accountsController.options)
property var notificationMemo: ({})
property var notificationRefreshPending: ({})
property bool notificationsPrimed: false
readonly property bool verticalFormFactor: Plasmoid.formFactor === PlasmaCore.Types.Vertical
readonly property var overviewProviderItems: overviewProviders()
readonly property bool globalNavigationAvailable: provider.length === 0
readonly property bool overviewAvailable: globalNavigationAvailable && providers.length > 1 && overviewProviderItems.length > 0
readonly property bool spendAvailable: globalNavigationAvailable && (costUsageEnabled || identifiedTokensEnabled)
readonly property bool sessionsAvailable: globalNavigationAvailable
readonly property int selectedProviderIndex: providerIndexForID(selectedProviderID)
readonly property bool globalViewSelected: selectionInitialized && selectedProviderID.length === 0
readonly property bool overviewSelected: overviewAvailable && globalViewSelected && selectedGlobalView === "overview"
readonly property bool spendSelected: spendAvailable && globalViewSelected && selectedGlobalView === "spend"
readonly property bool sessionsSelected: sessionsAvailable && globalViewSelected && selectedGlobalView === "sessions"
readonly property bool providerUsageFeedbackVisible: !spendSelected && !sessionsSelected
readonly property var selectedProviderData: providers.length > 0 && selectedProviderIndex >= 0
? providers[Math.min(selectedProviderIndex, providers.length - 1)]
: null
readonly property real roundedSurfaceRadius: Kirigami.Units.cornerRadius
+ Kirigami.Units.smallSpacing
// Radius for a small surface drawn inside a roundedSurfaceRadius container.
// Concentric rounding wants the inner radius reduced by the inset between
// the two edges, and popup surfaces inset their content by smallSpacing, so
// this is roundedSurfaceRadius minus that inset by construction.
readonly property real nestedSurfaceRadius: Kirigami.Units.cornerRadius
// Two-step de-emphasis scale for popup text. A supporting label uses the
// secondary step and the value it annotates uses the stronger step, so a
// label/value pair keeps its hierarchy without inventing a new opacity per
// section. 0.7 is the lowest step where Kirigami.Theme.textColor still
// clears WCAG AA 4.5:1 against the Breeze Light popup background.
readonly property real secondaryTextOpacity: 0.7
readonly property real valueTextOpacity: 0.85
// Shared track height for the credits, cost, and usage meters that stack in
// one provider detail view. Derived from gridUnit so it follows the user
// font size instead of pinning a device pixel count.
readonly property real meterTrackHeight: Math.round(Kirigami.Units.gridUnit * 0.4)
// Thinner track for meters that sit inside a scannable list row (overview
// rows, cost history rows) instead of leading a detail section. Keeping the
// two scales apart is what makes the primary meters read as primary.
readonly property real compactMeterTrackHeight: Math.round(Kirigami.Units.gridUnit * 0.28)
onUsageIdentityContextChanged: {
invalidateUsageData()
scheduleUsageRefresh()
}
onProviderOrderRawChanged: providers = ProviderOrder.orderedItems(
providers, providerOrderRaw)
onCostHistoryDaysChanged: applyTokenCosts()
onTokenCostsChanged: applyTokenCosts()
onAutoSelectProviderChanged: updateSelectedProvider()
onOverviewProviderIDsRawChanged: updateSelectedProvider()
onOverviewAvailableChanged: reconcileGlobalViewAvailability()
onSpendAvailableChanged: reconcileGlobalViewAvailability()
onSessionsAvailableChanged: reconcileGlobalViewAvailability()
onEnableNotificationsChanged: resetNotificationMemo()
onNotifyStatusIncidentsChanged: resetNotificationMemo()
onNotifyQuotaWarningsChanged: resetNotificationMemo()
onNotifyPredictivePaceWarningsChanged: resetNotificationMemo()
// The memo stores the level each row was last observed at. Keeping it across
// a threshold change would suppress a warning the new lower threshold should
// raise, and leave a row armed at a level the new higher threshold no longer
// reaches.
onQuotaWarningPercentChanged: resetNotificationMemo()
onQuotaCriticalPercentChanged: resetNotificationMemo()
onNotifyLimitResetsChanged: resetNotificationMemo()
onProvidersChanged: {
if (providers.length === 0) {
hoveredPanelProviderID = ""
updateSelectedProvider()
resetNotificationMemo()
return
}
if (hoveredPanelProviderID.length > 0 && providerIndexForID(hoveredPanelProviderID) < 0) {
hoveredPanelProviderID = ""
}
updateSelectedProvider()
Qt.callLater(processNotifications)
}
Component.onCompleted: {
usageLifecycleInitialized = true
forgetInstalledWidgetUpdate()
Qt.callLater(serviceWidgetUpdateRequest)
}
function safeMenuBarDisplayMode(value) {
return PanelDisplay.safeMode(value)
}
function safeCostHistoryMetric(value) {
return String(value || "cost") === "tokens" ? "tokens" : "cost"
}
function panelElementOrder() {
return PanelElements.normalizedOrder(panelElementOrderRaw)
}
function boundedConfigRevision(value) {
var revision = Number(value)
if (!isFinite(revision)) {
return 0
}
return Math.max(0, Math.min(2147480000, Math.floor(revision)))
}
function boundedCliMessage(value) {
return SafeText.cliMessage(SafeText.stripLoaderDiagnostics(value), SafeText.maximumCliMessageLength)
}
function isCliRecord(value) {
return Normalizer.isCliRecord(value)
}
function normalizedProviderID(value) {
return Normalizer.normalizedProviderID(value)
}
function hasOwnKey(item, key) {
return Guards.hasOwnKey(item, key)
}
function isUnsafeObjectKey(key) {
return Guards.isUnsafeObjectKey(key)
}
function providerMapKey(providerID) {
return Normalizer.providerSnapshotKey(providerID)
}
function refreshNow(bypassProviderRosterCache) {
usageController.refresh(bypassProviderRosterCache)
}
function scheduleUsageRefresh() {
usageController.scheduleRefresh()
}
function retryUsage() {
if (!loading) {
root.refreshNow(true)
}
}
function markUsageSnapshotReceived(nowMs) {
nowMs = nowMs === undefined ? Date.now() : nowMs
panelClockMs = nowMs
}
function usageCacheContext() {
if (providerConfigStamp.length === 0 || commandSource.length === 0) {
return ""
}
var accounts = Object.keys(selectedAccounts).sort().map(function(key) {
return [key, root.selectedAccounts[key]]
})
return Qt.md5(JSON.stringify([usageIdentityContext, accounts, providerConfigStamp]))
}
// The sole cache writer. Pure transitions decide data and label semantics;
// QML retains notification ordering, localization and configuration effects.
function applyUsageTransition(result) {
if (!result) {
return
}
if (result.refreshCompleted) {
markNotificationProvidersFresh(result.providers)
}
if (result.receivedFreshUsage) {
markUsageSnapshotReceived(result.nowMs)
}
providers = result.providers
switch (result.label) {
case "stale":
lastUpdatedText = i18n("Showing last known usage")
break
case "updated":
lastUpdatedText = i18n("Updated %1", timeLabels.clockTime(result.nowMs))
break
case "clear":
lastUpdatedText = ""
break
}
if (result.cacheValue !== null) {
Plasmoid.configuration.usageCache = result.cacheValue
}
if (result.advanceClock) {
panelClockMs = result.nowMs
}
}
function invalidateUsageData(providerID) {
if (!usageLifecycleInitialized) {
return
}
usageController.reset()
applyUsageTransition(UsageState.invalidated(providers, providerID,
providerID ? normalizeProvider({provider: providerID}) : null,
usageCacheContext(), Date.now()))
if (!providerID) {
accountsController.reset()
}
}
function restoreUsageCache() {
var context = usageCacheContext()
if (context.length === 0) {
return
}
var nowMs = Date.now()
var restored = UsageCache.decode(Plasmoid.configuration.usageCache, context, nowMs)
var cachedProviders = restored.map(function(payload) {
var item = root.normalizeProvider(payload)
item.lastGoodAtMs = Date.parse(payload.usage.updatedAt)
item.usageStale = true
item.tokenCost = null
return item
})
applyUsageTransition(UsageState.restored(providers, cachedProviders,
selectedAccounts, providerOrderRaw, context, nowMs))
}
function commitUsageSnapshot(items) {
applyUsageTransition(UsageState.accepted(providers, items, usageCacheContext(), Date.now()))
}
function failUsageRefresh(message) {
var nowMs = Date.now()
var failures = providers.map(function(item) {
return root.normalizeProvider(root.providerErrorPayload(item.provider, message))
})
applyUsageTransition(UsageState.failed(providers, failures, usageCacheContext(), nowMs))
}
function expireStaleUsage(nowMs) {
var expired = UsageCache.expiredProviderIDs(providers, nowMs)
if (expired.length === 0) {
return
}
var replacements = providers.filter(function(item) {
return expired.indexOf(item.provider) >= 0
}).map(function(item) {
return root.normalizeProvider(root.providerErrorPayload(item.provider,
item.error || i18n("Cached usage has expired. Refresh to try again.")))
})
applyUsageTransition(UsageState.expired(providers, replacements, usageCacheContext(), nowMs))
}
function lastGoodUsageText(item) {
var ageSeconds = Math.max(0, (panelClockMs - item.lastGoodAtMs) / 1000)
return ageSeconds < 60 ? i18n("Last known usage, just now")
: i18n("Last known usage, %1 ago", paceEtaText(ageSeconds))
}
function providerUsageTimestamp(item) {
if (!item || !(item.lastGoodAtMs > 0)) {
return ""
}
return item.usageStale === true ? lastGoodUsageText(item)
: i18n("Updated %1", timeLabels.clockTime(item.lastGoodAtMs))
}
function handleProviderConfigObservation(stamp, initial) {
providerConfigStamp = stamp
if (initial) {
restoreUsageCache()
return
}
invalidateUsageData()
scheduleUsageRefresh()
}
function refreshCost(force) {
return costController.refresh(force)
}
function refreshSpendIfStale() {
if (!spendSelected || !expanded) {
return false
}
return costController.refresh(false)
}
function refreshSessions() {
return sessionsController.refresh()
}
function providerErrorPayload(providerID, message) {
return {
provider: providerID,
source: source.length > 0 ? source : "auto",
error: {
code: 1,
kind: "provider",
message: message
}
}
}
function loadAccounts(providerID) {
return accountsController.load(providerID)
}
// Every session runs on the same machine unless the CLI reports more than
// one host, so a repeated host name would only add noise to each card.
readonly property bool sessionHostsVary: {
var firstHost = ""
for (var i = 0; i < sessions.length; i++) {
var host = sessions[i].host
if (host.length === 0) {
continue
}
if (firstHost.length === 0) {
firstHost = host
} else if (host !== firstHost) {
return true
}
}
return false
}
// "Just now", "5 minutes ago", "2 days ago": the popup's relative age.
function elapsedText(sinceMs, nowMs) {
var currentTimeMs = Number(nowMs)
if (!isFinite(currentTimeMs) || currentTimeMs <= 0) {
currentTimeMs = Date.now()
}
var elapsedSeconds = Math.max(0, Math.floor((currentTimeMs - sinceMs) / 1000))
if (elapsedSeconds < 60) {
return i18n("Just now")
}
var minutes = Math.floor(elapsedSeconds / 60)
if (minutes < 60) {
return i18np("%1 minute ago", "%1 minutes ago", minutes)
}
var hours = Math.floor(minutes / 60)
if (hours < 24) {
return i18np("%1 hour ago", "%1 hours ago", hours)
}
var days = Math.floor(hours / 24)
return i18np("%1 day ago", "%1 days ago", days)
}
function presentTokenCosts(snapshots) {
return costText.presentTokenCosts(snapshots)
}
function costHistoryWindowLabel(item, requestedHistoryDays) {
return costText.costHistoryWindowLabel(item, requestedHistoryDays)
}
function costChartPoints(points, memo) {
return CostPresentation.memoizedChartPoints(memo, costNumberFormat, points, costHistoryShowsTokens)
}
property var shareUsageSnapshot: null
readonly property var shareUsageWindow: shareUsageLoader.item
function openShareUsage() {
var costs = spendProviderCosts()
if (costs.length === 0 || costLoading) return
if (shareUsageWindow) shareUsageWindow.close()
shareUsageSnapshot = ShareUsage.snapshot(costs, costHistoryDays,
new Date().toISOString(), costErrorText.length > 0, costHistoryPeriod)
shareUsageLoader.active = true
shareUsageWindow.show()
shareUsageWindow.raise()
shareUsageWindow.requestActivate()
}
Loader {
id: shareUsageLoader
active: false
sourceComponent: Components.ShareUsageWindow {
snapshot: root.shareUsageSnapshot
applet: root
}
}
function spendProviderCosts() {
var snapshots = CostPresentation.spendSnapshots(tokenCosts, costHistoryDays, function(providerID) {
return providerTitle(providerID)
}, costHistoryPeriod)
return snapshots.map(function(item) { return root.costPresentation(item) })
}
function presentedSpendProviderCosts(costs) {
return ProviderOrder.orderedItems(costs || spendProviderCosts(), providerOrderRaw)
}
function spendDailyPoints() {
return CostPresentation.spendDailyPoints(costNumberFormat, spendProviderCosts(), costHistoryShowsTokens)
}
function spendCurrency(costs) {
return CostPresentation.spendCurrency(costs || spendProviderCosts())
}
function spendTotalLine() {
return costText.spendTotalLine(spendProviderCosts())
}
function updateCostTrustNoticeState(scope, summary, shouldDismiss) {
var transition = CostPresentation.costTrustNoticeStoreTransition(
summary, costTrustNoticeStates, scope, shouldDismiss)
costTrustNoticeStates = transition.states
return transition.state
}
function setCostHistoryDays(days) {
var nextDays = Math.max(1, Math.min(maximumCostHistoryPoints, Math.floor(Number(days) || 30)))
Plasmoid.configuration.costHistoryDays = nextDays
Plasmoid.configuration.costHistoryPeriod = ""
}
function setCostHistoryPeriod(period) {
Plasmoid.configuration.costHistoryPeriod = CostPresentation.costPeriod(period)
}
function setCostHistoryMetric(metric) {
Plasmoid.configuration.costHistoryMetric = safeCostHistoryMetric(metric)
}
// The CLI reports whether its local log scan already covers the requested
// window; until it does, the earliest bars are short for a scan reason
// rather than a spend reason.
function spendHistoryStillBuilding() {
return CostPresentation.historyStillBuilding(spendProviderCosts())
}
function dashboardLabelText(labelKey) {
return costText.dashboardLabelText(labelKey)
}
function dashboardPartText(part) {
return costText.dashboardPartText(part)
}
function dashboardDisplayRow(row) {
return costText.dashboardDisplayRow(row)
}
function providerTokenCost(providerID) {
var key = providerMapKey(providerID)
if (key.length === 0) {
return null
}
var snapshot = tokenCosts[key] || null
return CostPresentation.snapshotMatchesRange(snapshot, costHistoryDays, costHistoryPeriod)
? snapshot
: null
}
function applyTokenCosts() {
if (!providers || providers.length === 0) {
return
}
var nextProviders = []
for (var i = 0; i < providers.length; i++) {
var item = copyObject(providers[i])
item.tokenCost = item.usageStale === true ? null : providerTokenCost(item.provider)
nextProviders.push(item)
}
providers = nextProviders
}
function selectedAccountForProvider(providerID) {
var key = providerMapKey(providerID)
if (key.length === 0) {
return ""
}
var selected = selectedAccounts[key]
return selected ? String(selected) : ""
}
function accountOptionsForProvider(providerID) {
var key = providerMapKey(providerID)
if (key.length === 0) {
return []
}
return accountOptions[key] || []
}
function accountErrorForProvider(providerID) {
var key = providerMapKey(providerID)
if (key.length === 0) {
return ""
}
return privateErrorText(accountsController.errorForProvider(key))
}
function accountLoadingForProvider(providerID) {
var key = providerMapKey(providerID)
return key.length > 0 && accountsController.loadingForProvider(key)
}
function accountAvailabilityForProvider(providerID) {
var key = providerMapKey(providerID)
return key.length > 0 ? accountsController.availabilityForProvider(key) : ""
}
function privateErrorText(text) {
return PrivacyPresentation.errorText(text, privacyMode,
i18n("Details hidden by privacy mode."))
}
function accountDisplayLabel(item, index) {
return privacyMode ? i18n("Account %1", index + 1) : accountLabel(item)
}
function costPresentation(item) {
var result = PrivacyPresentation.cost(item, privacyMode)
if (!privacyMode || !result) {
return result
}
result.title = i18n("Cost")
result.sessionLine = costLine(i18n("Today"), result.today.cost,
result.today.tokens, result.today.currency)
var trust = CostPresentation.costTrustSummary([result])
result.valueMode = trust ? trust.valueMode : "plain"
result.windowLabel = costHistoryWindowLabel(result, result.historyDays)
result.monthLine = costLine(result.windowLabel,
result.totals.cost, result.totals.tokens, result.totals.currency,
result.valueMode)
result.windowValueLine = costValueLine(result.totals.cost, result.totals.tokens,
result.totals.currency, result.valueMode)
result.hintLine = tokenCostHint(result.provider)
for (var i = 0; i < result.models.length; i++) {
result.models[i].label = i18n("Model %1", i + 1)
}
for (var rankIndex = 0; rankIndex < result.tokenRanking.rows.length; rankIndex++) {
result.tokenRanking.rows[rankIndex].label = i18n("Model %1", rankIndex + 1)
}
for (var dayIndex = 0; dayIndex < result.daily.length; dayIndex++) {
var dayModels = result.daily[dayIndex].models
for (var modelIndex = 0; modelIndex < dayModels.length; modelIndex++) {
dayModels[modelIndex].label = i18n("Model %1", modelIndex + 1)
}
}
for (var j = 0; j < result.projects.rows.length; j++) {
result.projects.rows[j].label = i18n("Project %1", j + 1)
}
return result
}
function providerPresentations(items) {
return items.map(function(item) { return root.providerPresentation(item) })
}
function providerPresentation(item) {
if (!privacyMode || !item) {
return item
}
var result = PrivacyPresentation.provider(item, true, {
title: providerDisplayTitle(item.provider),
account: i18n("Account hidden"),
status: statusBadgeText(item.statusSeverity),
error: i18n("Details hidden by privacy mode."),
placeholder: i18n("No usage yet"),
usage: i18n("Usage"),
rowLabels: (item.rows || []).map(function(row) {
return rateWindowLabels.labelForLane(providerKey(item.provider), row.lane)
})
})
for (var i = 0; i < result.rows.length; i++) {
result.rows[i].pace = paceSummaryPartsText(result.rows[i].paceParts)
}
result.tokenCost = costPresentation(item.tokenCost)
return result
}
// Hidden rows and sections filter only the provider tab; fetching, alerts,
// the panel and the Overview summary keep their unfiltered data.
function popupDetailSections(item) {
return item ? PopupHiddenSections.visibleSections(item.providerDetails, popupHiddenDetailSections, item.provider) : []
}
function hiddenPopupDetailSections(item) {
return item ? PopupHiddenSections.hiddenSections(item.providerDetails, popupHiddenDetailSections, item.provider) : []
}
function popupDetailSectionHideable(section) {
return PopupHiddenSections.sectionKey(section).length > 0
&& popupHiddenDetailSections.length < PopupHiddenSections.maximumEntries
}
function hidePopupDetailSection(providerID, section) {
Plasmoid.configuration.popupHiddenDetailSections = PopupHiddenSections.serialize(
PopupHiddenSections.hidden(popupHiddenDetailSections, providerID, PopupHiddenSections.sectionKey(section)))
}
function restorePopupDetailSection(providerID, section) {
Plasmoid.configuration.popupHiddenDetailSections = PopupHiddenSections.serialize(
PopupHiddenSections.restored(popupHiddenDetailSections, providerID, PopupHiddenSections.sectionKey(section)))
}
function popupUsageRows(item) {
return item ? PopupHiddenRows.visibleRows(item.rows, popupHiddenUsageRows, item.provider) : []
}
function hiddenPopupUsageRows(item) {
return item ? PopupHiddenRows.hiddenRows(item.rows, popupHiddenUsageRows, item.provider) : []
}
function popupUsageRowHideable(row) {
return PopupHiddenRows.rowKey(row).length > 0
&& popupHiddenUsageRows.length < PopupHiddenRows.maximumEntries
}
function hidePopupUsageRow(providerID, row) {
var key = PopupHiddenRows.rowKey(row)
if (key.length > 0) {
Plasmoid.configuration.popupHiddenUsageRows = PopupHiddenRows.serialize(
PopupHiddenRows.hidden(popupHiddenUsageRows, providerID, key))
}
}
function restorePopupUsageRow(providerID, row) {
var key = PopupHiddenRows.rowKey(row)
if (key.length > 0) {
Plasmoid.configuration.popupHiddenUsageRows = PopupHiddenRows.serialize(
PopupHiddenRows.restored(popupHiddenUsageRows, providerID, key))
}
}
function accountLabel(item) {
return Normalizer.accountLabel(item)
}
// The stable account selection identity. Display labels may collapse spacing
// that still distinguishes two accounts, so selection, matching, and the
// CLI argument or enumerated Codex result compare keys, never labels.
function accountKey(item) {
return Normalizer.accountKey(item)
}
function accountSubtitle(item) {
if (privacyMode || !item) {
return ""
}
var parts = []
if (item.loginMethod && item.loginMethod.length > 0) {
parts.push(item.loginMethod)
}
if (item.organization && item.organization.length > 0 && item.organization !== item.account) {
parts.push(item.organization)
}
return parts.join(" · ")
}
function accountIsSelected(option, currentItem) {
if (!option) {
return false
}
var identity = accountKey(option)
var selected = selectedAccountForProvider(option.provider)
if (selected.length > 0) {
return identity.length > 0 && identity === selected
}
// A presented snapshot may replace the account label with a placeholder.
// Selection always compares against the original account identity.
var currentIndex = currentItem ? providerIndexForID(currentItem.provider) : -1
var accountItem = currentIndex >= 0 ? providers[currentIndex] : currentItem
return accountItem && accountItem.provider === option.provider && identity.length > 0 && identity === accountKey(accountItem)
}
function selectAccount(providerID, accountIdentity) {
var key = providerMapKey(providerID)
if (key.length === 0) {
return
}
var identity = String(accountIdentity || "")
var next = copyObject(selectedAccounts)
if (identity.length > 0) {
next[key] = identity
} else {
delete next[key]
}
invalidateUsageData(key)
selectedAccounts = next
setNotificationProviderRefreshPending(key, true)
var options = accountOptionsForProvider(key)
for (var i = 0; i < options.length; i++) {
if (root.accountKey(options[i]) === identity) {
replaceProviderSnapshot(key, options[i])
scheduleUsageRefresh()
return
}
}
// Coalesce this request with the controller's changed account inputs.
scheduleUsageRefresh()
}
function replaceProviderSnapshot(providerID, snapshot) {
var key = providerMapKey(providerID)
if (key.length === 0) {
return
}
var replacement = UsageCache.reconcile([], [snapshot], Date.now())[0]
replacement.tokenCost = replacement.usageStale === true ? null : providerTokenCost(key)
var nextProviders = []
for (var i = 0; i < providers.length; i++) {
nextProviders.push(providers[i].provider === key ? replacement : providers[i])
}
if (!nextProviders.some(function(item) { return item.provider === key })) {
nextProviders.push(replacement)
}
providers = ProviderOrder.orderedItems(nextProviders, providerOrderRaw)
}
function presentAccountOptions(options) {
var result = ({})
var keys = Object.keys(options)
for (var i = 0; i < keys.length; i++) {
result[keys[i]] = options[keys[i]].map(function(item) {
return root.presentProviderSnapshot(item)
})
}
return result
}
function normalizeProvider(item) {
return presentProviderSnapshot(ProviderSnapshot.normalize(item, Date.now()))
}
function presentProviderSnapshot(snapshot) {
var providerID = snapshot.provider
var dashboard = snapshot.usageDashboard
return ProviderViewModel.presented(snapshot, {
title: providerTitle(providerID,
snapshot.displayName === null ? providerDisplayNames[providerID] : snapshot.displayName),
rows: snapshot.rows.map(function(row) { return root.presentUsageWindow(row, providerID) }),
usageDashboard: dashboard ? {kpis: dashboard.kpis.map(dashboardDisplayRow), rows: dashboard.rows.map(dashboardDisplayRow)} : null,
providerCost: providerCostSection(providerID, snapshot.providerCost),
resetCredits: resetCreditsSection(providerID, snapshot.resetCredits),
tokenCost: providerTokenCost(providerID),
planText: planText(providerID, snapshot.planMethod),
dashboardUrl: providerDashboardUrl(providerID),
statusUrl: safeStatusUrl(providerID, snapshot.statusUrl),
changelogUrl: providerChangelogUrl(providerID),
status: snapshot.statusRecord ? statusText(snapshot.statusRecord) : "",
error: snapshot.commandFailed ? (snapshot.error || i18n("codexbar command failed.")) : "",
placeholder: snapshot.placeholder === "limitsUnavailable" ? i18n("Limits not available")
: (snapshot.placeholder === "noUsage" ? i18n("No usage yet") : "")
})
}
function subscriptionDateText(date) {
var value = timeLabels.subscriptionDate(date)
return date.kind === "renews" ? i18n("Plan renews on %1", value) : i18n("Plan expires on %1", value)
}
function presentUsageWindow(snapshot, providerID) {
var row = copyObject(snapshot)
row.label = snapshot.label !== null ? snapshot.label
: (snapshot.lane === "extra" ? i18n("Extra") : rateWindowLabels.labelForLane(providerKey(providerID), snapshot.lane, snapshot.cliLabel))
row.reset = Normalizer.boundedDisplayText(resetText({resetsAt: snapshot.resetValue,
resetDescription: snapshot.resetDescription}, false), 500)
row.pace = paceSummaryPartsText(snapshot.paceParts)
delete row.resetValue
return row
}
// The popup pace line. Its run-out forecast counts down from the row's
// observation like the panel, instead of repeating the receipt-time ETA.
function usagePaceText(row) {
if (!row || !row.pace) {
return ""
}
return Array.isArray(row.paceParts)
? paceSummaryPartsText(PacePresentation.advancedParts(row.paceParts, row.paceObservedAtMs, panelClockMs))
: row.pace
}
function providerCostSection(providerID, cost) {
return providerCostDetails.costSection(providerID, cost)
}
function resetCreditsSection(providerID, resetCredits) {
return providerCostDetails.resetSection(providerID, resetCredits)
}
function codexCreditLimitUsageRow(creditLimit) {
return providerCostDetails.creditLimitRow(creditLimit)
}
function resetText(window, absolute) {
return usageWindowText.resetText(window, panelClockMs, absolute)
}
function usageResetText(row) {
row = PrivacyPresentation.quota(row, privacyMode, "")
if (!Normalizer.canPresentQuotaDetails(row)) {
return ""
}
if (row.resetsAt || row.resetDescription) {
return resetText({
resetsAt: row.resetsAt || "",
resetDescription: row.resetDescription || ""
}, resetTimesShowAbsolute)
}
var fallback = String(row.reset || "")
return fallback.length === 0 && row.hasPercent
&& Normalizer.usageConfidence(row.dataConfidence) === "percentOnly"
? i18n("Reset unavailable") : fallback
}
function statusText(status) {
// Status fields are CLI-controlled: read them without coercing objects,
// whose missing toString would throw inside String().
var indicator = Normalizer.safeScalarText(status.indicator)
var description = Normalizer.safeScalarText(status.description).trim()
if (indicator.length === 0 || indicator === "none") {
return description
}
var labels = {
"minor": i18n("Partial outage"),
"major": i18n("Major outage"),
"critical": i18n("Critical issue"),
"maintenance": i18n("Maintenance"),
"unknown": i18n("Status unknown")
}
var text = Guards.hasOwnKey(labels, indicator) ? labels[indicator] : indicator
return description.length > 0 ? text + ": " + description : text
}
function statusBadgeColor(severity) {
switch (String(severity || "")) {
case "critical":
case "major":
return Kirigami.Theme.negativeTextColor
case "minor":
case "maintenance":
return Kirigami.Theme.neutralTextColor
case "unknown":
return Kirigami.Theme.textColor
default:
return "transparent"
}
}
function statusBadgeText(severity) {
switch (String(severity || "")) {
case "critical":
return i18n("Critical")
case "major":
return i18n("Major")
case "minor":
return i18n("Issue")
case "maintenance":
return i18n("Maint.")
case "unknown":
return i18n("Unknown")
default:
return ""
}
}
function primaryIncidentProvider() {
var best = null
var bestRank = 0
for (var i = 0; i < providers.length; i++) {
var item = providers[i]
if (!item || item.statusKnown === false || item.hasIncident !== true) {
continue
}
var rank = NotificationMemo.severityRank(item.statusSeverity)
if (rank > bestRank) {
best = item
bestRank = rank
}
}
return best
}
function quotaWarningMarkers(row) {
if (!showQuotaWarningMarkers || !row || !row.hasPercent || !Normalizer.authoritativeQuota(row)) {
return []
}
return QuotaThresholds.markers(
quotaWarningPercent,
quotaCriticalPercent,
usageBarsShowUsed)
}
// The thresholds used to surface only as two ticks on the provider detail
// meter, so an almost exhausted window looked exactly like an idle one on
// the panel and in the overview. Every meter fill reads its colour from
// this one level; the provider accent stays in charge below the warning
// step, and the same setting that hides the markers hides the colour.
function quotaSeverity(row) {
if (!showQuotaWarningMarkers || !row || !row.hasPercent || !Normalizer.authoritativeQuota(row)) {
return ""
}
return QuotaThresholds.level(row.usedPercent, quotaWarningPercent, quotaCriticalPercent)
}
function quotaMeterColor(row, accent) {
var severity = quotaSeverity(row)
return severity.length > 0 ? statusBadgeColor(severity) : accent
}
// Quota, pace, and reset memo state is threshold-derived and has to be
// rebuilt whenever a setting changes. Provider status is not: a settings
// change is not a status transition, so the status baseline survives the
// reset. Dropping it would either re-announce an ongoing incident or, once
// the first observation is silently primed, swallow an incident that starts
// while the provider is still refreshing.
function resetNotificationMemo() {
notificationMemo = NotificationPlanner.transition(
[], notificationMemo, ({ mode: "reset" })).nextMemo
notificationsPrimed = false
// Prime synchronously against the current snapshot so an incident that
// starts between the reset and the next deferred pass cannot be
// absorbed silently into the new baseline. The deferred call stays as
// a coalesced follow-up for refreshes already in flight.
processNotifications()
Qt.callLater(processNotifications)
}
function notificationProviderRefreshPending(providerID) {
var key = providerMapKey(providerID)
return key.length > 0 && notificationRefreshPending[key] === true
}
function setNotificationProviderRefreshPending(providerID, pending) {
var key = providerMapKey(providerID)
if (key.length === 0) {
return
}
var nextPending = copyObject(notificationRefreshPending)
if (pending) {
nextPending[key] = true
} else {
delete nextPending[key]
}
notificationRefreshPending = nextPending
}
function markNotificationProvidersFresh(items) {
var nextPending = copyObject(notificationRefreshPending)
for (var i = 0; i < items.length; i++) {
var item = items[i]
if (!item || (item.usageStale === true && item.statusKnown !== true)) {
continue
}
var providerID = providerMapKey(item.provider)
if (providerID.length === 0) {
continue
}
var selectedAccount = selectedAccountForProvider(providerID)
if (selectedAccount.length > 0 && accountKey(item) !== selectedAccount) {
continue
}
// A failed account refresh can still carry fresh provider status.
// The planner separately ignores missing quota evidence.
delete nextPending[providerID]
}
notificationRefreshPending = nextPending
}
function notificationScopeKey(item) {
if (!item) {
return JSON.stringify(["", ""])
}
var providerID = providerMapKey(item.provider)
var selectedAccount = selectedAccountForProvider(providerID)
var currentAccount = selectedAccount.length > 0 ? selectedAccount : accountKey(item)
return JSON.stringify([providerID, currentAccount])
}
function paceWarningActive(row) {
return row && Normalizer.authoritativeQuota(row) && row.paceOnTop === false && Number(row.paceEtaSeconds) > 0
}
function paceSummaryText(pace) {
return paceSummaryPartsText(PacePresentation.summaryParts(pace))
}
function paceSummaryPartsText(parts) {
return usageWindowText.paceSummaryPartsText(parts)
}
function paceEtaText(seconds) {
return usageWindowText.paceEtaText(seconds)
}
// Usage at or above this percent arms a row for reset detection; once armed,
// dropping to or below the floor fires a single "limit reset" notification.
// Mirrors the macOS weekly-limit reset detector, scoped to limits the user
// was actually near so routine short-window resets stay quiet.
readonly property int limitResetArmThreshold: 80
readonly property int limitResetFloor: 5
function quotaNotificationLevel(row) {
if (!row || !row.hasPercent || !Normalizer.authoritativeQuota(row)) {
return ""
}
return QuotaThresholds.level(
row.usedPercent,
quotaWarningPercent,
quotaCriticalPercent)
}
// QML resolves account identity, refresh freshness, configured thresholds,
// and the rows to display. The pure planner receives only semantic
// observations and returns ordered intents; it never sees i18n or effects.
function notificationObservationRows(item) {
if (item && item.usageStale === true) {
return []
}
var sourceRows = item && Array.isArray(item.rows) ? item.rows : []
var result = []
for (var i = 0; i < sourceRows.length; i++) {
var row = sourceRows[i]
result.push({
lane: row && row.lane ? String(row.lane) : "",
windowId: row ? Normalizer.extraWindowID(row.windowId) : "",
label: row && row.label ? String(row.label) : "",
resetsAt: row && row.resetsAt ? String(row.resetsAt) : "",
hasPercent: row && row.hasPercent === true,
dataConfidence: row ? row.dataConfidence : "",
usedPercent: row ? Number(row.usedPercent) : NaN,
quotaLevel: quotaNotificationLevel(row),
paceKnown: row && row.paceKnown === true,
paceActive: paceWarningActive(row)
})
}
return result
}
function notificationObservations() {
var result = []
for (var i = 0; i < providers.length; i++) {
var item = providers[i]
if (!item) {
continue
}
var rows = notificationObservationRows(item)
result.push({
providerIndex: i,
providerID: providerMapKey(item.provider),
scopeID: notificationScopeKey(item),
pending: NotificationPlanner.observationPending(
(item.usageStale === true && item.statusKnown !== true)
|| notificationProviderRefreshPending(item.provider),
String(item.error || "").length > 0,
item.statusKnown === true,
rows.length),
errorPresent: String(item.error || "").length > 0,
usageStale: item.usageStale === true,
statusKnown: item.statusKnown === true,
statusActive: item.hasIncident === true
&& String(item.statusSeverity || "").length > 0
&& String(item.status || "").length > 0,
statusSeverity: String(item.statusSeverity || ""),
statusIncidentKey: String(item.statusIncidentKey || ""),
rows: rows
})
}
return result
}
function notificationPlannerOptions(mode) {
return {
mode: mode,
statusEnabled: includeStatus && notifyStatusIncidents,
quotaEnabled: notifyQuotaWarnings,
paceEnabled: notifyPredictivePaceWarnings,
resetEnabled: notifyLimitResets,
resetArmThreshold: limitResetArmThreshold,
resetFloor: limitResetFloor
}
}
function dispatchNotificationIntents(intents, observations) {
for (var i = 0; i < intents.length; i++) {
var intent = intents[i]
var observation = observations[intent.observationIndex]
var item = observation ? providers[observation.providerIndex] : null
if (!item) continue
var rows = Array.isArray(item.rows) ? item.rows : []
var row = rows[intent.rowIndex]
var resetLine = row && intent.kind === "quota" ? resetLabel(usageResetText(row)) : ""
var paceEta = row && intent.kind === "pace" ? paceEtaText(row.paceEtaSeconds) : ""
var message = providerNotificationText.message(intent, item, row, resetLine, paceEta)
if (message) sendPlasmaNotification(message.title, message.body, message.urgency)
}
}
function processNotifications() {
if (!enableNotifications || providers.length === 0) {
return
}
var observations = notificationObservations()
var mode = notificationsPrimed ? "observe" : "prime"
var result = NotificationPlanner.transition(
observations,
notificationMemo,
notificationPlannerOptions(mode))
// Commit the full transition before running any external effect. A
// re-entrant refresh cannot observe the old baseline and notify twice.
notificationMemo = result.nextMemo
notificationsPrimed = true
dispatchNotificationIntents(result.intents, observations)
}
function sendPlasmaNotification(title, body, urgency, actionLabel) {
return updateNotifications.send(title, body, urgency, actionLabel)
}
// Settings request manual widget updates through this runtime key, and the
// applet runs them, so closing the dialog cannot cut an install short.
readonly property string widgetUpdateRequest: Plasmoid.configuration.widgetUpdateRequest || ""
// Deferred: servicing rewrites the key this binding reads. A synchronous
// write re-enters the binding, which Qt drops as a loop, leaving a stale
// "check"/"install" that restarts the updater after every finished run.
onWidgetUpdateRequestChanged: Qt.callLater(root.serviceWidgetUpdateRequest)
// Plasma exposes KPluginMetaData at runtime without a declarative QML type.
readonly property var appletContext: Plasmoid
readonly property string installedWidgetVersion: appletContext && appletContext.metaData
? String(appletContext.metaData.version || "") : ""
// A found release stays offered only while it is newer than this widget,
// so upgrading another way does not leave an Install button for it.
function restoredWidgetUpdateVersion() {
return UpdateLogic.restoredAvailableVersion(
Plasmoid.configuration.widgetUpdateAvailableVersion || "", installedWidgetVersion)
}
function forgetInstalledWidgetUpdate() {
var restored = restoredWidgetUpdateVersion()
if (restored !== (Plasmoid.configuration.widgetUpdateAvailableVersion || "")) {
Plasmoid.configuration.widgetUpdateAvailableVersion = restored
}
}
function serviceWidgetUpdateRequest() {
var request = widgetUpdateRequest
if (widgetUpdater.busy || request.length === 0) {
return
}
if (request === "check" || request === "install") {
// Start first: busy then keeps the marker write below from re-entering.
widgetUpdater.runNow(request === "install")
if (widgetUpdater.busy) {
Plasmoid.configuration.widgetUpdateRequest = "running:" + request
return
}
}
// A finished run, a stale marker from a previous session, or junk.
Plasmoid.configuration.widgetUpdateRequest = ""
}
function notifyAvailableUpdate(version, url, releaseUrl) {
updateNotifications.notifyAvailableUpdate(version, url, releaseUrl)
}
function notifyInstalledUpdate(version) {
updateNotifications.notifyInstalledUpdate(version)
}
function planText(providerID, method) {
if (providerKey(providerID) === "codex" && method.length > 0) {
return capitalize(method)
}
return ""
}
function providerKey(value) {
return ProviderIdentity.resolveProviderKey(value)
}
function providerCliArgument(value) {
return ProviderIdentity.providerCliArgument(value)
}
function providerDisplayTitle(value) {
return providerTitle(value, "", privacyMode)
}
function providerTitle(value, displayName, privateTitle) {
var key = providerKey(value)
// Optional display names are CLI-controlled and may carry structured
// values: their primitive conversion throws and would discard the
// whole provider snapshot. Only truthy scalars reach String();
// anything else degrades to the bundled fallback, keeping the quota.
var rawDisplay = displayName || ""
if (typeof rawDisplay !== "string" && typeof rawDisplay !== "number" && typeof rawDisplay !== "boolean") {
rawDisplay = ""
}
var preferred = privateTitle === true ? "" : String(rawDisplay).trim()
if (preferred.length > 0) {
return preferred
}
return providerNames.titleForKey(key, privateTitle === true ? i18n("Provider") : "")
}
function providerIconSource(value) {
var fileName = ProviderIdentity.providerIconFileName(value)
if (fileName.length === 0) {
return "view-statistics"
}
return Qt.resolvedUrl("../icons/providers/" + fileName)
}
function providerIconIsMask(value) {
return true
}
// Whether the rendered icon can stand in for the provider's name. Bundled
// icons and brand colors cover the same providers, so a provider outside
// that set falls back to one generic icon in the theme highlight and is
// indistinguishable from every other provider outside it.
function providerIconIdentifies(value) {
return ProviderIdentity.providerBrandColorChannels(value).length === 3
}
function providerColor(value) {
var channels = ProviderIdentity.providerBrandColorChannels(value)
if (channels.length !== 3) {
return Kirigami.Theme.highlightColor
}
return Qt.rgba(channels[0], channels[1], channels[2], 1)
}
function providerDashboardUrl(providerID) {
return ProviderIdentity.providerDashboardUrl(providerID)
}
function providerDocsUrl(providerID) {
return ProviderIdentity.providerDocsUrl(providerID)
}
function providerLoginUrl(providerID) {
return ProviderIdentity.providerLoginUrl(providerID)
}
function providerStatusUrl(providerID) {
return ProviderIdentity.providerStatusUrl(providerID)
}
function safeStatusUrl(providerID, url) {
return Normalizer.safeStatusUrl(providerStatusUrl(providerID), url)
}
function providerChangelogUrl(providerID) {
switch (providerKey(providerID)) {
case "codex":
return "https://github.com/openai/codex/releases"
case "claude":
return "https://github.com/anthropics/claude-code/releases"
case "gemini":
return "https://github.com/google-gemini/gemini-cli/releases"
case "grok":
return "https://x.ai/news"
default:
return ""
}
}
function actionRows(item) {
if (!item) {
return []
}
// This menu uses raw provider capabilities and account presence only.
// Titles are local literals; never render identity or provider prose here.
var rows = []
rows.push({
title: accountLoadingForProvider(item.provider) ? i18n("Loading accounts…") : i18n("Accounts…"),
icon: "user-identity",
action: "accounts",
enabled: !accountLoadingForProvider(item.provider)
&& accountAvailabilityForProvider(item.provider) !== "unsupported"
})
var accountAction = providerAccountAction(item)
if (accountAction) {
rows.push(accountAction)
}
if (item.dashboardUrl && item.dashboardUrl.length > 0) {
rows.push({ title: i18n("Usage Dashboard"), icon: "view-statistics", action: "dashboard", enabled: true })
}
if (safeStatusUrl(item.provider, item.statusUrl).length > 0) {
rows.push({ title: i18n("Status Page"), icon: "network-connect", action: "status", enabled: true })
}
if (showProviderChangelogs && item.changelogUrl && item.changelogUrl.length > 0) {
rows.push({ title: i18n("Changelog"), icon: "view-list-details", action: "changelog", enabled: true })
}
var docsUrl = providerDocsUrl(item.provider)
if (docsUrl.length > 0) {
rows.push({ title: i18n("Docs"), icon: "help-contents", action: "docs", url: docsUrl, enabled: true })
}
rows.push({ title: i18n("Refresh"), icon: "view-refresh", action: "refresh", enabled: true, separatorBefore: true })
rows.push({ title: i18n("Settings…"), icon: "configure", action: "settings", enabled: true })
rows.push({ title: i18n("About CodexBar"), icon: "help-about", action: "about", enabled: true })
return rows
}
function providerAccountAction(item) {
var title = item.account && item.account.length > 0 ? i18n("Switch Account…") : i18n("Add Account…")
var loginUrl = providerLoginUrl(item.provider)
switch (providerKey(item.provider)) {
case "devin":
return { title: i18n("Open Devin…"), icon: "internet-services", action: "account-url", url: "https://app.devin.ai/settings/usage", enabled: true }
case "factory":
return { title: i18n("Open Droid in Browser…"), icon: "internet-services", action: "account-url", url: "https://app.factory.ai", enabled: true }
case "manus":
return { title: title, icon: "internet-services", action: "account-url", url: "https://manus.im", enabled: true }
case "mimo":
return { title: title, icon: "internet-services", action: "account-url", url: "https://platform.xiaomimimo.com/api/v1/genLoginUrl?currentPath=%2F%23%2Fconsole%2Fbalance", enabled: true }
case "perplexity":
return { title: title, icon: "internet-services", action: "account-url", url: "https://www.perplexity.ai/", enabled: true }
default:
return loginUrl.length > 0
? { title: title, icon: "internet-services", action: "account-url", url: loginUrl, enabled: true }
: null
}
}
function aiInsightsLanguageTag() {
return AiInsights.languageTag(i18nc(
"BCP 47 language tag of this translation, such as it or pt-BR. AI Insights are written in this language.", "en"))
}
function generateAiInsight() {
aiInsightsController.generate()
}
function aiInsightsProviderName(provider) {
return aiInsightsMessages.providerName(provider)
}
function aiInsightsErrorText(reason) {
return aiInsightsMessages.errorText(reason, aiInsightsController.requestContext.provider)
}
function performAction(actionRow) {
var actionID = actionRow && actionRow.action ? actionRow.action : actionRow
var item = selectedProviderData
if (actionID === "dashboard" && item) {
Qt.openUrlExternally(item.dashboardUrl)
} else if (actionID === "status" && item) {
Qt.openUrlExternally(safeStatusUrl(item.provider, item.statusUrl))
} else if (actionID === "changelog" && item) {
Qt.openUrlExternally(item.changelogUrl)
} else if (actionID === "docs" && actionRow && actionRow.url) {
Qt.openUrlExternally(actionRow.url)
} else if (actionID === "accounts" && item) {
root.loadAccounts(item.provider)
} else if (actionID === "account-url" && actionRow && actionRow.url) {
Qt.openUrlExternally(actionRow.url)
} else if (actionID === "refresh") {
root.refreshNow(true)
} else if (actionID === "about") {
Qt.openUrlExternally("https://github.com/steipete/CodexBar")
} else if (actionID === "settings") {
var action = Plasmoid.internalAction("configure")
if (action) {
action.trigger()
}
}
}
function withAlpha(color, alpha) {
return Qt.rgba(color.r, color.g, color.b, alpha)
}
function canvasColor(color, alpha) {
var opacity = alpha === undefined ? color.a : alpha
return "rgba("
+ Math.round(color.r * 255) + ", "
+ Math.round(color.g * 255) + ", "
+ Math.round(color.b * 255) + ", "
+ opacity + ")"
}
function contrastTextColor(color) {
var luminance = (0.2126 * color.r) + (0.7152 * color.g) + (0.0722 * color.b)
return luminance > 0.62 ? Qt.rgba(0.08, 0.08, 0.1, 1) : Qt.rgba(1, 1, 1, 1)
}
function readableAccentColor(accent, background) {
var surface = background || Kirigami.Theme.backgroundColor
return ThemeContrast.readableAccentColor(
accent,
surface,
Kirigami.Theme.textColor)
}
function providerReadableColor(value, background) {
return readableAccentColor(
providerColor(value),
background || Kirigami.Theme.backgroundColor)
}
function copyObject(item) {
return Guards.copyObject(item)
}
function capitalize(value) {
var text = String(value || "")
if (text.length === 0) {
return ""
}
return text.charAt(0).toUpperCase() + text.slice(1)
}
// Four- and five-figure totals are unreadable as a bare digit run, and
// Number.toLocaleString with 'f' localizes the decimal mark without adding
// group separators, so the grouping is applied here.
function groupedDecimalString(value, digits) {
return CostPresentation.groupedDecimalString(costNumberFormat, value, digits)
}
function amountString(value, currency) {
return CostPresentation.amountString(costNumberFormat, value, currency)
}
// Same figures as costLine without the window label, for surfaces that
// already state the range once (the Usage & Spend range selector).
function qualifiedCostValue(value, valueMode) {
return costText.qualifiedCostValue(value, valueMode)
}
function costValueLine(cost, tokens, currency, valueMode) {
return costText.costValueLine(cost, tokens, currency, valueMode)
}
function costLine(label, cost, tokens, currency, valueMode) {
return costText.costLine(label, cost, tokens, currency, valueMode)
}
function tokenCountString(tokens) {
return CostPresentation.tokenCountString(tokens, costNumberFormat)
}
function usageCountText(value, unit) {
return costText.usageCountText(value, unit)
}
function tokenCostHint(providerID) {
return costText.tokenCostHint(providerID)
}
function switcherCandidateRows(item) {
return PanelDisplay.candidateRows(item ? item.rows : null,
item ? providerKey(item.provider) : "",
item && Normalizer.authoritativeQuota(item) ? item.providerCost : null,
i18n("Included plan"))
}
function panelDisplayRow(item, mode) {
return PanelDisplay.rowForMode(switcherCandidateRows(item), mode, panelQuotaLane)
}
function panelMeterRows(item) {
return PanelDisplay.meterRows(switcherCandidateRows(item), panelQuotaLane)
}
function panelMeterDescription(item) {
var description = panelMeterRows(item).map(function(row) {
return root.quotaDescription(row)
}).join(". ")
if (!item || item.usageStale !== true) {
return description
}
var lastKnown = lastGoodUsageText(item)
return description.length > 0 ? i18n("%1 - %2", description, lastKnown) : lastKnown
}
function switcherMetricRow(item) {
// Popup tabs retain their automatic quota independently of panel settings.
return PanelDisplay.rowForMode(switcherCandidateRows(item), "percent")
}
function switcherPercent(item) {
var row = switcherMetricRow(item)
return row ? displayPercent(row) : -1
}
// Overview rows draw a labelled meter per quota lane, independent of the
// panel's lane choice.
function overviewMeterRows(item) {
return PanelDisplay.overviewMeterRows(switcherCandidateRows(item))
}
function quotaDescription(row) {
var text = i18n("%1: %2", row.label, usagePercentText(row))
var reset = resetTextForRow(row)
return reset.length > 0 ? i18n("%1 - %2", text, reset) : text
}
// A popup tab shows its quota only as an underline and its failure only
// by dimming, so screen readers get both as text.
function switcherDescription(item) {
if (!item) {
return ""
}
var parts = []
var row = switcherMetricRow(item)
if (row && row.hasPercent) {
parts.push(quotaDescription(row))
}
if (item.usageStale === true) {
parts.push(lastGoodUsageText(item))
}
if (item.error && item.error.length > 0) {
parts.push(item.error)
}
return parts.join(". ")
}
// Eligibility, the stored selection, and the visible limit all live in
// OverviewProviders, so the settings checkboxes and the rendered rows
// resolve the same canonical provider IDs.
function overviewProviders() {
return OverviewProviders.visibleItems(providers, overviewProviderIDsRaw)
}
function providerIndex(item) {
return item ? providerIndexForID(item.provider) : -1
}
function providerIndexForID(providerID) {
var id = String(providerID || "")
if (id.length === 0 || !providers) {
return -1
}
for (var i = 0; i < providers.length; i++) {
if (providers[i] && providers[i].provider === id) {
return i
}
}
return -1
}
function openProviderFromPanel(providerID) {
var index = providerIndexForID(providerID)
if (index < 0) {
return false
}
selectedProviderID = providers[index].provider
selectionInitialized = true
expanded = true
return true
}
function providerPlaceholderText(item) {
return OverviewProviders.placeholderText(item)
}
function usagePercentText(row) {
return usageWindowText.percentText(row, usageBarsShowUsed)
}
function usageConfidenceText(row) {
return usageWindowText.confidenceText(row)
}
function displayPercent(row) {
if (!row || !row.hasPercent) {
return 0
}
return usageBarsShowUsed ? row.usedPercent : row.leftPercent
}
function paceMarkerPercent(row) {
if (!row || row.pacePercent < 0 || !Normalizer.canPresentQuotaDetails(row)) {
return -1
}
return usageBarsShowUsed ? row.pacePercent : clamp(100 - row.pacePercent, 0, 100)
}
function percentSuffix() {
return usageBarsShowUsed ? i18n("used") : i18n("left")
}
function resetLabel(value) {
return usageWindowText.resetLabel(value)
}
function providerCountText(count) {
var total = Math.max(0, Math.round(Number(count) || 0))
return i18np("%1 provider", "%1 providers", total)
}
function clamp(value, minimum, maximum) {
return Normalizer.clamp(value, minimum, maximum)
}
function selectedCompactProvider() {
if (providers.length === 0) {
return null
}
var panelItems = panelProviderItems()
var selected = selectedProviderIndex >= 0 ? providers[selectedProviderIndex] : null
return PopupSelection.compactPanelProvider(
autoSelectProvider, selected, panelItems,
autoSelectedProviderIndex(panelItems))
}
function globalViewAvailability() {
return {
overview: overviewAvailable,
spend: spendAvailable,
sessions: sessionsAvailable
}
}
function selectGlobalView(viewID) {
var candidate = String(viewID || "")
if (!PopupSelection.globalViewIsAvailable(candidate, globalViewAvailability())) {
return
}
selectedGlobalView = candidate
selectedProviderID = ""
selectionInitialized = true
if (candidate === "spend") {
refreshSpendIfStale()
}
}
function updateSelectedProvider() {
var firstProviderID = providers && providers.length > 0
? providers[0].provider
: ""
var automaticProviderID = firstProviderID.length > 0
? providers[autoSelectedProviderIndex(providers)].provider
: ""
var next = PopupSelection.reconcile({
providerID: selectedProviderID,
globalView: selectedGlobalView,
initialized: selectionInitialized
}, {
autoSelect: autoSelectProvider,
currentProviderExists: selectedProviderIndex >= 0,
firstProviderID: firstProviderID,
automaticProviderID: automaticProviderID,
globalViews: globalViewAvailability()
})
selectedProviderID = next.providerID
selectedGlobalView = next.globalView
selectionInitialized = next.initialized
}
function reconcileGlobalViewAvailability() {
if (PopupSelection.globalSelectionNeedsReconciliation({
providerID: selectedProviderID,
globalView: selectedGlobalView,
initialized: selectionInitialized
}, globalViewAvailability())) {
updateSelectedProvider()
}
}
// Usage and incident severity decide the automatic selection in
// ProviderAutoSelect; the roster it ranks is the panel-filtered list for
// the panel and the full roster for the popup.
function autoSelectedProviderIndex(items) {
return ProviderAutoSelect.bestIndex(Array.isArray(items) ? items : providers)
}
// The panel-only provider selection. The popup, notifications, and every
// other surface keep the full roster; only compact rendering narrows here.
function panelProviderItems() {
return PanelProviders.filteredItems(providers, panelProviderIDsRaw)
}
function compactProviders() {
if (!providers || Plasmoid.configuration.showMultiProviderInPanel === false) {
return []
}
var result = []
var panelItems = panelProviderItems()
for (var i = 0; i < panelItems.length && result.length < PanelProviders.maximumSelectableProviders; i++) {
var rows = panelMeterRows(panelItems[i])
if (PanelRules.matchesAny(panelVisibilityRules.meters, rows, panelClockMs)) {
result.push(providerPresentation(panelItems[i]))
}
}
return result
}
// The panel label as separable segments. The compact renderer surrenders
// whole segments when the meter row leaves it too little room, so it needs
// them apart; every other caller joins them back through compactText().
function compactTextSegments() {
var item = providerPresentation(selectedCompactProvider())
var row = panelDisplayRow(item, menuBarDisplayMode)
return panelText.segments(item,
PanelRules.matches(panelVisibilityRules.text, row, panelClockMs),
PanelProviders.selectionActive(panelProviderIDsRaw),
item ? menuBarDisplayText(item) : "",
item ? providerIconIdentifies(item.provider) : false)
}
function compactText() {
return panelText.fullText(compactTextSegments())
}
function setHoveredPanelProvider(providerID) {
hoveredPanelProviderID = String(providerID || "")
}
function clearHoveredPanelProvider(providerID) {
var id = String(providerID || "")
if (hoveredPanelProviderID === id) {
hoveredPanelProviderID = ""
}
}
function hoveredPanelProvider() {
var id = String(hoveredPanelProviderID || "")
if (id.length === 0 || !providers) {
return null
}
// Visibility rules can filter a hovered meter out of the rendered
// compactProviders() while its provider stays in providers, and its
// destroyed MouseArea delivers no reliable hover-exit for cleanup.
// Narrow the tooltip only while the meter is actually rendered.
var rendered = compactProviders().some(function(meter) {
return meter && meter.provider === id
})
if (!rendered) {
return null
}
for (var i = 0; i < providers.length; i++) {
if (providers[i] && providers[i].provider === id) {
return providers[i]
}
}
return null
}
function panelProviderToolTipText(item) {
var presented = providerPresentation(item)
return panelText.providerToolTipText(presented, panelMeterDescription(presented))
}
function panelToolTipText() {
var hovered = hoveredPanelProvider()
var hoveredLine = hovered ? panelProviderToolTipText(hovered) : ""
if (hoveredLine.length > 0) {
return hoveredLine
}
var lines = []
var roster = panelProviderItems()
for (var i = 0; i < roster.length && lines.length < 6; i++) {
var line = panelProviderToolTipText(roster[i])
if (line.length > 0) {
lines.push(line)
}
}
return panelText.toolTipText(lines,
privateErrorText(Normalizer.boundedDisplayText(errorText, 500)))
}
function menuBarDisplayText(item) {
if (!item) {
return ""
}
var mode = String(menuBarDisplayMode || "percent")
var row = panelDisplayRow(item, mode)
return panelText.displayText(row, mode,
mode === "resetTime" ? resetTextForRow(row) : "",
mode === "runOut" ? runOutTextForRow(row) : "")
}
// Duration-only forecast token. It stays empty unless the CLI actually
// predicts exhaustion before the reset, so the panel never shows a
// countdown the pace data does not support.
// Account options retain their own receipt time across later refreshes.
function runOutTextForRow(row) {
if (!PanelDisplay.rowHasRunOut(row)) {
return ""
}
return paceEtaText(PanelDisplay.remainingSeconds(
row.paceEtaSeconds, row.paceObservedAtMs, panelClockMs))
}
function resetTextForRow(row) {
var reset = usageResetText(row)
if (reset.length === 0) {
return ""
}
return resetLabel(reset)
}
// Credit balances are plain counts, so they share the popup's grouped,
// locale-aware figure formatting instead of printing a bare digit run with
// a hardcoded decimal mark. A whole balance keeps no fractional part, so a
// depleted account reads as "0" rather than "0.0".
function formatNumber(value) {
return CostPresentation.formatCount(costNumberFormat, value)
}
Timer {
id: panelClockTimer
interval: root.panelClockIntervalMs
repeat: true
running: root.providers.length > 0
triggeredOnStart: false
onTriggered: {
root.panelClockMs = Date.now()
root.expireStaleUsage(root.panelClockMs)
}
}
Components.CostText {
id: costText
numberFormat: root.costNumberFormat
}
Components.UsageWindowText {
id: usageWindowText
dateLabels: timeLabels
}
Components.ProviderNotificationText {
id: providerNotificationText
}
Components.ProviderCostDetails {
id: providerCostDetails
numberFormat: root.costNumberFormat
}
Components.PanelText {
id: panelText
loading: root.loading
usageBarsShowUsed: root.usageBarsShowUsed
showProvider: Plasmoid.configuration.showProviderInPanel
showPercent: Plasmoid.configuration.showPercentInPanel
showCredits: Plasmoid.configuration.showCreditsInPanel
numberFormat: root.costNumberFormat
}
Components.ProviderNames {
id: providerNames
}
Components.RateWindowLabels {
id: rateWindowLabels
}
Components.TimeLabels {
id: timeLabels
}
Components.AiInsightsMessages {
id: aiInsightsMessages
}
Controllers.ProviderConfigWatcher {
id: providerConfigWatcher
active: root.usageLifecycleInitialized
onStampObserved: function(stamp, initial) {
root.handleProviderConfigObservation(stamp, initial)
}
}
Controllers.UpdateNotificationsController {
id: updateNotifications
configuration: Plasmoid.configuration
enableNotifications: root.enableNotifications
privacyMode: root.privacyMode
onReleasePageRequested: function(url) {
Qt.openUrlExternally(url)
}
}
Controllers.UsageController {
id: usageController
commandPath: root.commandPath
provider: root.provider
sourceMode: root.source
includeStatus: root.includeStatus
selectedAccounts: root.selectedAccounts
providerConfigRevision: root.providerConfigRevision
providerConfigStamp: root.providerConfigStamp
providerOrderRaw: root.providerOrderRaw
refreshIntervalSec: root.refreshIntervalSec
refreshOnOpen: root.refreshOnOpen
popupVisible: root.expanded
onSnapshotReceived: function(items) {
root.commitUsageSnapshot(items.map(function(item) { return root.presentProviderSnapshot(item) }))
root.applyTokenCosts()
}
onFailed: function(message) { root.failUsageRefresh(message) }
onEmptyRoster: root.invalidateUsageData()
}
Controllers.AccountsController {
id: accountsController
commandPath: root.commandPath
sourceMode: root.source
includeStatus: root.includeStatus
}
Controllers.CostController {
id: costController
commandPath: root.commandPath
provider: root.provider
historyDays: root.costHistoryDays
historyPeriod: root.costHistoryPeriod
costUsageEnabled: root.costUsageEnabled
active: root.spendSelected && root.expanded
}
Controllers.IdentifiedTokensController {
id: identifiedTokensController
commandPath: root.commandPath
collectionEnabled: root.identifiedTokensEnabled
sshHost: Plasmoid.configuration.identifiedTokensSshHost || ""
historyDays: Math.max(1, Math.min(365, Number(Plasmoid.configuration.identifiedTokensHistoryDays) || 30))
}
Controllers.SessionsController {
id: sessionsController
commandPath: root.commandPath
refreshIntervalSec: root.refreshIntervalSec
active: root.expanded && root.sessionsSelected
}
Controllers.AiInsightsController {
id: aiInsightsController
insightsEnabled: root.aiInsightsEnabled
provider: Plasmoid.configuration.aiInsightsProvider || "ollama"
model: Plasmoid.configuration.aiInsightsModel || ""
endpoint: Plasmoid.configuration.aiInsightsOllamaEndpoint || ""
zdr: Plasmoid.configuration.aiInsightsOpenRouterZdr !== false
language: root.aiInsightsLanguage
intervalHours: AiInsights.intervalHours(Plasmoid.configuration.aiInsightsIntervalHours)
snapshotText: root.aiInsightsSnapshot.text
snapshotId: root.aiInsightsSnapshot.id
cacheText: root.aiInsightsEnabled ? (Plasmoid.configuration.aiInsightsCache || "") : ""
lastAttempt: Plasmoid.configuration.aiInsightsLastAttempt || ""
rateLimit: Plasmoid.configuration.aiInsightsRateLimit || ""
onAttemptStarted: function(timestamp) {
Plasmoid.configuration.aiInsightsLastAttempt = timestamp
}
onRateLimitStored: function(text) {
Plasmoid.configuration.aiInsightsRateLimit = text
}
onGenerated: function(text) {
Plasmoid.configuration.aiInsightsCache = text
}
}
Controllers.ManagedCliController {
commandPath: root.commandPath
automaticUpdates: Plasmoid.configuration.cliAutomaticUpdates === true
onChanged: {
// A new CLI version can change which providers support accounts.
accountsController.reset()
root.refreshNow(true)
}
}
Controllers.CliUpdateController {
commandPath: root.commandPath
automaticChecks: Plasmoid.configuration.cliUpdateChecksEnabled === true
lastCheck: Plasmoid.configuration.cliUpdateLastCheck || ""
onCheckedRelease: function(timestamp) {
Plasmoid.configuration.cliUpdateLastCheck = timestamp
}
onUpdateAvailable: function(version, releaseUrl) {
updateNotifications.notifyAvailableCliUpdate(version, releaseUrl)
}
}
Controllers.WidgetUpdateController {
id: widgetUpdater
updateChecksEnabled: Plasmoid.configuration.updateChecksEnabled !== false
autoUpdateEnabled: Plasmoid.configuration.autoUpdateEnabled === true
autoUpdateIntervalHours: isFinite(Number(Plasmoid.configuration.autoUpdateIntervalHours))
? Math.max(1, Math.min(168, Number(Plasmoid.configuration.autoUpdateIntervalHours))) : 24
autoUpdateLastCheck: Plasmoid.configuration.autoUpdateLastCheck || ""
initialAvailableVersion: root.restoredWidgetUpdateVersion()
onStatusRecorded: function(statusText, errorText, availableVersion) {
Plasmoid.configuration.widgetUpdateLastStatus = statusText
Plasmoid.configuration.widgetUpdateLastError = errorText
Plasmoid.configuration.widgetUpdateAvailableVersion = availableVersion
}
// Deferred: busy drops inside the controller's own completion handling.
onBusyChanged: if (!busy) Qt.callLater(root.serviceWidgetUpdateRequest)
onCheckSucceeded: function(timestamp) {
Plasmoid.configuration.autoUpdateLastCheck = timestamp
}
onUpdateAvailable: function(version, assetUrl, releaseUrl) {
root.notifyAvailableUpdate(version, assetUrl, releaseUrl)
}
onUpdateInstalled: function(version) {
root.notifyInstalledUpdate(version)
}
}
compactRepresentation: Components.CompactRepresentation {
applet: root
}
fullRepresentation: Components.FullRepresentation {
applet: root
}
}