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
38 changes: 38 additions & 0 deletions Sources/CodexBar/EnvironmentalImpactSectionView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import SwiftUI

struct EnvironmentalImpactSectionView: View {
let lines: [String]
let hintLine: String?
let textFont: Font

@Environment(\.menuItemHighlighted) private var isHighlighted

var body: some View {
if !self.lines.isEmpty {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Image(systemName: "leaf")
.foregroundColor(.green)
.imageScale(.small)
Text("environmental_impact_header")
.font(.body)
.fontWeight(.medium)
}
.padding(.top, 4)

ForEach(self.lines, id: \.self) { line in
Text(line)
.font(self.textFont)
}

if let hintLine, !hintLine.isEmpty {
Text(hintLine)
.font(.footnote)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(3)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
}
46 changes: 45 additions & 1 deletion Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,57 @@ extension UsageMenuCardView.Model {
}
return "\(windowLabel): \(monthCost)"
}()

let energySessionLine: String?
let co2SessionLine: String?
if let sTokens = snapshot.sessionTokens {
let impact = EnvironmentalImpact(tokens: sTokens)
energySessionLine = L(
"environmental_impact_energy_today",
UsageFormatter.formatEnergy(impact.energyKWh),
impact.smartphoneCharges,
impact.kettleBoils)
co2SessionLine = L(
Comment on lines +65 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Bedrock billing-day semantics in eco session text

For .bedrock, sessionTokens represents the latest billing day (not necessarily today), but this new branch always formats the session eco line with the environmental_impact_*_today copy. That makes the environmental row claim “Today” even when the data is from an older day, which is a user-visible accuracy regression specific to Bedrock and inconsistent with the existing sessionLine logic that already uses a billing-day label.

Useful? React with 👍 / 👎.

"environmental_impact_co2_today",
UsageFormatter.formatCO2(impact.co2Kg),
impact.carKm)
} else {
energySessionLine = nil
co2SessionLine = nil
}

let energyMonthLine: String?
let co2MonthLine: String?
if let mTokens = monthTokensValue {
let impact = EnvironmentalImpact(tokens: mTokens)
energyMonthLine = L(
"environmental_impact_energy_month",
windowLabel,
UsageFormatter.formatEnergy(impact.energyKWh),
impact.smartphoneCharges,
impact.kettleBoils)
co2MonthLine = L(
"environmental_impact_co2_month",
windowLabel,
UsageFormatter.formatCO2(impact.co2Kg),
impact.carKm)
} else {
energyMonthLine = nil
co2MonthLine = nil
}

let err = (error?.isEmpty ?? true) ? nil : error
return TokenUsageSection(
sessionLine: sessionLine,
monthLine: monthLine,
hintLine: Self.tokenUsageHint(provider: provider),
errorLine: err,
errorCopyText: (error?.isEmpty ?? true) ? nil : error)
errorCopyText: (error?.isEmpty ?? true) ? nil : error,
energySessionLine: energySessionLine,
co2SessionLine: co2SessionLine,
energyMonthLine: energyMonthLine,
co2MonthLine: co2MonthLine,
environmentalImpactHintLine: L("environmental_impact_hint"))
}

static func tokenUsageHint(provider: UsageProvider) -> String? {
Expand Down
53 changes: 53 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// swiftlint:disable file_length
import AppKit
import CodexBarCore
import SwiftUI
Expand Down Expand Up @@ -86,6 +87,46 @@ struct UsageMenuCardView: View {
let hintLine: String?
let errorLine: String?
let errorCopyText: String?
let energySessionLine: String?
let co2SessionLine: String?
let energyMonthLine: String?
let co2MonthLine: String?
let environmentalImpactHintLine: String?

init(
sessionLine: String,
monthLine: String,
hintLine: String? = nil,
errorLine: String? = nil,
errorCopyText: String? = nil,
energySessionLine: String? = nil,
co2SessionLine: String? = nil,
energyMonthLine: String? = nil,
co2MonthLine: String? = nil,
environmentalImpactHintLine: String? = nil)
{
self.sessionLine = sessionLine
self.monthLine = monthLine
self.hintLine = hintLine
self.errorLine = errorLine
self.errorCopyText = errorCopyText
self.energySessionLine = energySessionLine
self.co2SessionLine = co2SessionLine
self.energyMonthLine = energyMonthLine
self.co2MonthLine = co2MonthLine
self.environmentalImpactHintLine = environmentalImpactHintLine
}

var environmentalImpactLines: [String] {
[
self.energySessionLine,
self.co2SessionLine,
self.energyMonthLine,
self.co2MonthLine,
]
.compactMap(\.self)
.filter { !$0.isEmpty }
}
}

struct ProviderCostSection {
Expand Down Expand Up @@ -197,6 +238,12 @@ struct UsageMenuCardView: View {
.font(.footnote)
Text(tokenUsage.monthLine)
.font(.footnote)

EnvironmentalImpactSectionView(
lines: tokenUsage.environmentalImpactLines,
hintLine: tokenUsage.environmentalImpactHintLine,
textFont: .footnote)

if let hint = tokenUsage.hintLine, !hint.isEmpty {
Text(hint)
.font(.footnote)
Expand Down Expand Up @@ -621,6 +668,12 @@ struct UsageMenuCardCostSectionView: View {
.font(.caption)
Text(tokenUsage.monthLine)
.font(.caption)

EnvironmentalImpactSectionView(
lines: tokenUsage.environmentalImpactLines,
hintLine: tokenUsage.environmentalImpactHintLine,
textFont: .caption)

if let hint = tokenUsage.hintLine, !hint.isEmpty {
Text(hint)
.font(.footnote)
Expand Down
25 changes: 25 additions & 0 deletions Sources/CodexBar/PreferencesProviderDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ struct ProviderDetailView<SupplementaryContent: View>: View {
}
if self.model.tokenUsage != nil {
metricLabels.append(L("Cost"))
metricLabels.append(L("Eco"))
}

let infoWidth = ProviderSettingsMetrics.labelWidth(
Expand Down Expand Up @@ -428,6 +429,30 @@ struct ProviderMetricsInlineView: View {
title: "",
value: tokenUsage.monthLine,
labelWidth: self.labelWidth)
if let energySession = tokenUsage.energySessionLine {
ProviderMetricInlineTextRow(
title: L("Eco"),
value: energySession,
labelWidth: self.labelWidth)
}
if let co2Session = tokenUsage.co2SessionLine {
ProviderMetricInlineTextRow(
title: "",
value: co2Session,
labelWidth: self.labelWidth)
}
if let energyMonth = tokenUsage.energyMonthLine {
ProviderMetricInlineTextRow(
title: "",
value: energyMonth,
labelWidth: self.labelWidth)
}
if let co2Month = tokenUsage.co2MonthLine {
ProviderMetricInlineTextRow(
title: "",
value: co2Month,
labelWidth: self.labelWidth)
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBar/Resources/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,12 @@
/* Cost estimation */
"cost_header_estimated" = "Cost (estimat)";
"cost_estimate_hint" = "Estimat a partir de registres locals · pot diferir de la teva factura";

/* Environmental Footprint */
"environmental_impact_header" = "Estimació ambiental";
"environmental_impact_energy_today" = "Avui: %1$@ (%2$d càrregues de mòbil / %3$d bullides de bullidor)";
"environmental_impact_co2_today" = "CO₂e d'avui: %1$@ (~%2$.1f km conduint)";
"environmental_impact_energy_month" = "%1$@: %2$@ (%3$d càrregues de mòbil / %4$d bullides de bullidor)";
"environmental_impact_co2_month" = "%1$@ CO₂e: %2$@ (~%3$.1f km conduint)";
"environmental_impact_hint" = "Estimació aproximada a partir dels tokens; l'impacte real varia segons el model, el maquinari i la xarxa elèctrica.";
"Eco" = "Eco";
9 changes: 9 additions & 0 deletions Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,12 @@
/* Cost estimation */
"cost_header_estimated" = "Cost (estimated)";
"cost_estimate_hint" = "Estimated from local logs · may differ from your bill";

/* Environmental Footprint */
"environmental_impact_header" = "Environmental estimate";
"environmental_impact_energy_today" = "Today: %1$@ (%2$d phone charges / %3$d kettle boils)";
"environmental_impact_co2_today" = "Today CO₂e: %1$@ (~%2$.1f km driving)";
"environmental_impact_energy_month" = "%1$@: %2$@ (%3$d phone charges / %4$d kettle boils)";
"environmental_impact_co2_month" = "%1$@ CO₂e: %2$@ (~%3$.1f km driving)";
"environmental_impact_hint" = "Rough estimate from token count; actual impact varies by model, hardware, and grid mix.";
"Eco" = "Eco";
9 changes: 9 additions & 0 deletions Sources/CodexBar/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,12 @@
/* Cost estimation */
"cost_header_estimated" = "Coste (estimado)";
"cost_estimate_hint" = "Estimado a partir de registros locales · puede diferir de tu factura";

/* Environmental Footprint */
"environmental_impact_header" = "Estimación ambiental";
"environmental_impact_energy_today" = "Hoy: %1$@ (%2$d cargas de móvil / %3$d hervidos de hervidor)";
"environmental_impact_co2_today" = "CO₂e de hoy: %1$@ (~%2$.1f km conduciendo)";
"environmental_impact_energy_month" = "%1$@: %2$@ (%3$d cargas de móvil / %4$d hervidos de hervidor)";
"environmental_impact_co2_month" = "%1$@ CO₂e: %2$@ (~%3$.1f km conduciendo)";
"environmental_impact_hint" = "Estimación aproximada a partir de tokens; el impacto real varía según el modelo, el hardware y la red eléctrica.";
"Eco" = "Eco";
9 changes: 9 additions & 0 deletions Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,12 @@
/* Cost estimation */
"cost_header_estimated" = "Custo (estimado)";
"cost_estimate_hint" = "Estimado a partir de logs locais · pode diferir da sua fatura";

/* Environmental Footprint */
"environmental_impact_header" = "Estimativa ambiental";
"environmental_impact_energy_today" = "Hoje: %1$@ (%2$d cargas de celular / %3$d fervuras de chaleira)";
"environmental_impact_co2_today" = "CO₂e de hoje: %1$@ (~%2$.1f km dirigindo)";
"environmental_impact_energy_month" = "%1$@: %2$@ (%3$d cargas de celular / %4$d fervuras de chaleira)";
"environmental_impact_co2_month" = "%1$@ CO₂e: %2$@ (~%3$.1f km dirigindo)";
"environmental_impact_hint" = "Estimativa aproximada a partir dos tokens; o impacto real varia conforme modelo, hardware e matriz elétrica.";
"Eco" = "Eco";
9 changes: 9 additions & 0 deletions Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,12 @@
"copilot_device_code" = "设备代码已复制到剪贴板:%1$@\n\n请在以下地址验证:%2$@";
"copilot_waiting_text" = "请在浏览器中完成登录。\n登录完成后,此窗口会自动关闭。";
"vertex_ai_login_instructions" = "要跟踪 Vertex AI 用量,请通过 Google Cloud 进行认证。\n\n1. 打开终端\n2. 运行:gcloud auth application-default login\n3. 按照浏览器提示登录\n4. 设置你的项目:gcloud config set project PROJECT_ID\n\n是否现在打开终端?";

/* Environmental Footprint */
"environmental_impact_header" = "环境估算";
"environmental_impact_energy_today" = "今日:%1$@(约 %2$d 次手机充电 / %3$d 次烧水)";
"environmental_impact_co2_today" = "今日 CO₂e:%1$@(约驾车 %2$.1f 公里)";
"environmental_impact_energy_month" = "%1$@:%2$@(约 %3$d 次手机充电 / %4$d 次烧水)";
"environmental_impact_co2_month" = "%1$@ CO₂e:%2$@(约驾车 %3$.1f 公里)";
"environmental_impact_hint" = "基于 token 数的粗略估算;实际影响会随模型、硬件和电网结构变化。";
"Eco" = "生态";
32 changes: 24 additions & 8 deletions Sources/CodexBar/StatusItemController+CostMenuCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,30 @@ extension StatusItemController {
}

static func costMenuTooltipLines(tokenUsage: UsageMenuCardView.Model.TokenUsageSection?) -> [String] {
[
tokenUsage?.sessionLine,
tokenUsage?.monthLine,
tokenUsage?.hintLine,
tokenUsage?.errorLine,
]
.compactMap(\.self)
.filter { !$0.isEmpty }
var lines: [String] = []
if let tokenUsage {
lines.append(tokenUsage.sessionLine)
lines.append(tokenUsage.monthLine)
if let energySession = tokenUsage.energySessionLine {
lines.append(energySession)
}
if let co2Session = tokenUsage.co2SessionLine {
lines.append(co2Session)
}
if let energyMonth = tokenUsage.energyMonthLine {
lines.append(energyMonth)
}
if let co2Month = tokenUsage.co2MonthLine {
lines.append(co2Month)
}
if let hint = tokenUsage.hintLine, !hint.isEmpty {
lines.append(hint)
}
if let error = tokenUsage.errorLine, !error.isEmpty {
lines.append(error)
}
}
return lines.filter { !$0.isEmpty }
}

static func costMenuVisibleDetailLines(tokenUsage: UsageMenuCardView.Model.TokenUsageSection?) -> [String] {
Expand Down
35 changes: 35 additions & 0 deletions Sources/CodexBarCore/EnvironmentalImpact.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

public struct EnvironmentalImpact: Sendable, Equatable {
private enum Estimate {
static let joulesPerToken = 12.0
static let joulesPerKWh = 3_600_000.0
static let globalAverageCO2KgPerKWh = 0.385
static let smartphoneChargesPerKWh = 75.0
static let kettleBoilsPerKWh = 10.0
static let averageCarCO2KgPerKm = 0.12
}

public let energyKWh: Double
public let co2Kg: Double

public init(tokens: Int) {
// Broad estimate only; real usage varies by model, hardware, batching, and grid mix.
let joules = Double(tokens) * Estimate.joulesPerToken
self.energyKWh = joules / Estimate.joulesPerKWh

self.co2Kg = self.energyKWh * Estimate.globalAverageCO2KgPerKWh
}

public var smartphoneCharges: Int {
Int(round(self.energyKWh * Estimate.smartphoneChargesPerKWh))
}

public var kettleBoils: Int {
Int(round(self.energyKWh * Estimate.kettleBoilsPerKWh))
}

public var carKm: Double {
self.co2Kg / Estimate.averageCarCO2KgPerKm
}
}
25 changes: 25 additions & 0 deletions Sources/CodexBarCore/UsageFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,29 @@ public enum UsageFormatter {
}
return cleaned
}

public static func formatEnergy(_ kwh: Double) -> String {
let wh = kwh * 1000.0
if wh < 1000.0 {
return "\(self.compactDecimal(wh)) Wh"
}
return "\(self.compactDecimal(kwh)) kWh"
}

public static func formatCO2(_ kg: Double) -> String {
let g = kg * 1000.0
if g < 1000.0 {
return "\(self.compactDecimal(g)) g"
}
return "\(self.compactDecimal(kg)) kg"
}

private static func compactDecimal(_ value: Double) -> String {
if value >= 10.0 {
return String(format: "%.0f", value.rounded())
}
var formatted = String(format: "%.1f", value)
if formatted.hasSuffix(".0") { formatted.removeLast(2) }
return formatted
}
}
Loading