Skip to content

Commit dcc2ad9

Browse files
author
Peter Steinberger
committed
Merge remote-tracking branch 'origin/main' into pry-2623
2 parents a5451e1 + 00ef23c commit dcc2ad9

27 files changed

Lines changed: 2163 additions & 51 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- z.ai/GLM: route BigModel aliases and relay-file keys only to China endpoints, reject canonical cross-region overrides before bearer auth, and keep Kimi browser import disabled when Cookie Source is Off (#2351). Thanks @Leehow!
77
- Kimi: enrich Code API and CLI usage with the monthly membership pool from a signed-in Kimi Desktop session, using WAL-safe read-only cookie access (#2351). Thanks @Leehow!
88
- Kimi/GLM: distinguish Kimi Code from the regional Open Platform, bind China and international keys to their issuing hosts, and show GLM Coding Plan's 5-hour window as primary with MCP separate (#2351). Thanks @Leehow!
9+
- Provider plugins: declarative detail rows/charts plus bundled JavaScript conversions for OpenAI, z.ai, OpenRouter, Poe, and ClawRouter behind `CODEXBAR_JS_PROVIDERS=1`.
910

1011
### Changed
1112
- Menu: move each usage window's used percentage and reset time into its title row, with all pace detail on one line (#2182). Thanks @jack24254029!

Sources/CodexBar/MenuCardHeightFingerprint.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import CodexBarCore
12
import Foundation
23

34
extension UsageMenuCardView.Model {
@@ -22,6 +23,7 @@ extension UsageMenuCardView.Model {
2223
"codexResetCredits=\(self.codexResetCredits?.heightFingerprint ?? "")",
2324
"metrics=\(MenuCardHeightFingerprint.join(self.metrics.map(\.heightFingerprint)))",
2425
"notes=\(notesFingerprint)",
26+
"providerDetails=\(self.providerDetails.heightFingerprint)",
2527
"dashboard=\(self.inlineUsageDashboard?.heightFingerprint ?? "")",
2628
"providerCost=\(self.providerCost?.heightFingerprint ?? "")",
2729
"tokenUsage=\(self.tokenUsage?.heightFingerprint ?? "")",
@@ -34,6 +36,31 @@ extension UsageMenuCardView.Model {
3436
}
3537
}
3638

39+
extension [ProviderDetailSection] {
40+
fileprivate var heightFingerprint: String {
41+
MenuCardHeightFingerprint.join(self.map { section in
42+
MenuCardHeightFingerprint.join([
43+
MenuCardHeightFingerprint.field("title", section.title),
44+
MenuCardHeightFingerprint.join(section.rows.map { row in
45+
MenuCardHeightFingerprint.join([
46+
MenuCardHeightFingerprint.field("label", row.label),
47+
MenuCardHeightFingerprint.field("value", row.value),
48+
MenuCardHeightFingerprint.field("secondary", row.secondaryValue),
49+
])
50+
}),
51+
section.chart.map { chart in
52+
MenuCardHeightFingerprint.join([
53+
chart.kind.rawValue,
54+
MenuCardHeightFingerprint.field("title", chart.title),
55+
MenuCardHeightFingerprint.field("unit", chart.unit),
56+
"points=\(chart.points.count)",
57+
])
58+
} ?? "chart=nil",
59+
])
60+
})
61+
}
62+
}
63+
3764
private enum MenuCardHeightFingerprint {
3865
private static let hashSalt = UUID()
3966

Sources/CodexBar/MenuCardView+ModelHelpers.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ extension UsageMenuCardView.Model {
279279
self.subtitleStyle == .error &&
280280
self.metrics.isEmpty &&
281281
self.usageNotes.isEmpty &&
282+
self.providerDetails.isEmpty &&
282283
self.openAIAPIUsage == nil &&
283284
self.inlineUsageDashboard == nil &&
284285
self.creditsRemaining == nil &&
@@ -290,6 +291,7 @@ extension UsageMenuCardView.Model {
290291
var hasUsageContent: Bool {
291292
!self.metrics.isEmpty ||
292293
!self.usageNotes.isEmpty ||
294+
!self.providerDetails.isEmpty ||
293295
self.openAIAPIUsage != nil ||
294296
self.inlineUsageDashboard != nil ||
295297
self.codexResetCredits != nil ||
@@ -301,6 +303,7 @@ extension UsageMenuCardView.Model {
301303
self.inlineUsageDashboard != nil &&
302304
self.metrics.isEmpty &&
303305
self.usageNotes.isEmpty &&
306+
self.providerDetails.isEmpty &&
304307
self.openAIAPIUsage == nil &&
305308
self.codexResetCredits == nil &&
306309
self.placeholder == nil
@@ -337,6 +340,7 @@ extension UsageMenuCardView.Model {
337340
guard self.provider == candidate.provider,
338341
!includeMetrics || self.metrics.count == candidate.metrics.count,
339342
self.usageNotes == candidate.usageNotes,
343+
self.providerDetails == candidate.providerDetails,
340344
(self.openAIAPIUsage == nil) == (candidate.openAIAPIUsage == nil),
341345
Self.hasCompatibleCreditsLayout(
342346
currentText: self.creditsText,

Sources/CodexBar/MenuCardView.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ struct UsageMenuCardView: View {
162162
let metrics: [Metric]
163163
let usageNotes: [String]
164164
var subscriptionNotes: [String] = []
165+
var providerDetails: [ProviderDetailSection] = []
165166
let openAIAPIUsage: OpenAIAPIUsageSnapshot?
166167
let inlineUsageDashboard: InlineUsageDashboardModel?
167168
let creditsText: String?
@@ -214,6 +215,11 @@ struct UsageMenuCardView: View {
214215
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
215216
.font(.subheadline)
216217
}
218+
if !liveModel.providerDetails.isEmpty {
219+
ProviderDetailSectionsContent(
220+
sections: liveModel.providerDetails,
221+
chartColor: liveModel.progressColor)
222+
}
217223
} else {
218224
let hasUsage = liveModel.hasUsageContent
219225
let hasCredits = liveModel.creditsText != nil
@@ -674,6 +680,11 @@ private struct UsageMenuCardUsageContentView: View {
674680
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
675681
.font(.subheadline)
676682
}
683+
if !self.model.providerDetails.isEmpty {
684+
ProviderDetailSectionsContent(
685+
sections: self.model.providerDetails,
686+
chartColor: self.model.progressColor)
687+
}
677688
if self.showBottomDivider {
678689
Divider()
679690
}
@@ -960,6 +971,7 @@ extension UsageMenuCardView.Model {
960971
metrics: metrics,
961972
usageNotes: usageNotes,
962973
subscriptionNotes: Self.subscriptionMetadataNotes(snapshot: input.snapshot, provider: input.provider),
974+
providerDetails: input.snapshot?.details ?? [],
963975
openAIAPIUsage: openAIAPIUsage,
964976
inlineUsageDashboard: inlineUsageDashboard,
965977
creditsText: creditsText,
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import CodexBarCore
2+
import SwiftUI
3+
4+
struct ProviderDetailSectionsContent: View {
5+
let sections: [ProviderDetailSection]
6+
let chartColor: Color
7+
@Environment(\.menuItemHighlighted) private var isHighlighted
8+
9+
var body: some View {
10+
VStack(alignment: .leading, spacing: 12) {
11+
ForEach(Array(self.sections.enumerated()), id: \.offset) { index, section in
12+
if index > 0 {
13+
Divider()
14+
}
15+
self.section(section)
16+
}
17+
}
18+
.frame(maxWidth: .infinity, alignment: .leading)
19+
}
20+
21+
private func section(_ section: ProviderDetailSection) -> some View {
22+
VStack(alignment: .leading, spacing: 6) {
23+
if let title = section.title {
24+
Text(title)
25+
.font(.caption.weight(.semibold))
26+
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
27+
.textCase(.uppercase)
28+
.lineLimit(1)
29+
}
30+
ForEach(Array(section.rows.enumerated()), id: \.offset) { _, row in
31+
HStack(alignment: .firstTextBaseline, spacing: 8) {
32+
Text(row.label)
33+
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
34+
Spacer(minLength: 8)
35+
VStack(alignment: .trailing, spacing: 1) {
36+
Text(row.value)
37+
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
38+
.fontWeight(.medium)
39+
if let secondaryValue = row.secondaryValue {
40+
Text(secondaryValue)
41+
.font(.caption2)
42+
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
43+
}
44+
}
45+
}
46+
.font(.caption)
47+
.lineLimit(1)
48+
}
49+
if let chart = section.chart {
50+
ProviderDetailChartContent(chart: chart, color: self.chartColor)
51+
}
52+
}
53+
}
54+
}
55+
56+
private struct ProviderDetailChartContent: View {
57+
let chart: ProviderDetailSection.Chart
58+
let color: Color
59+
@Environment(\.menuItemHighlighted) private var isHighlighted
60+
61+
var body: some View {
62+
VStack(alignment: .leading, spacing: 3) {
63+
if self.chart.title != nil || self.chart.unit != nil {
64+
HStack(spacing: 6) {
65+
if let title = self.chart.title {
66+
Text(title)
67+
.font(.caption2.weight(.semibold))
68+
}
69+
Spacer(minLength: 0)
70+
if let unit = self.chart.unit {
71+
Text(unit)
72+
.font(.caption2)
73+
.monospacedDigit()
74+
}
75+
}
76+
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
77+
.lineLimit(1)
78+
}
79+
Group {
80+
switch self.chart.kind {
81+
case .bars:
82+
self.bars
83+
case .line:
84+
self.line
85+
}
86+
}
87+
.frame(height: 58)
88+
.accessibilityElement(children: .combine)
89+
.accessibilityLabel(self.accessibilityLabel)
90+
}
91+
}
92+
93+
private var bars: some View {
94+
GeometryReader { geometry in
95+
let scale = UsageChartScale(values: self.chart.points.map(\.value))
96+
HStack(alignment: .bottom, spacing: 2) {
97+
ForEach(Array(self.chart.points.enumerated()), id: \.offset) { _, point in
98+
RoundedRectangle(cornerRadius: 1.5, style: .continuous)
99+
.fill(self.fillColor(value: point.value, scale: scale))
100+
.frame(maxWidth: .infinity)
101+
.frame(height: Self.height(value: point.value, scale: scale, available: geometry.size.height))
102+
}
103+
}
104+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
105+
.overlay(alignment: .bottomLeading) {
106+
Rectangle()
107+
.fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(0.22))
108+
.frame(height: 1)
109+
}
110+
}
111+
}
112+
113+
private var line: some View {
114+
GeometryReader { _ in
115+
Canvas { context, size in
116+
let scale = UsageChartScale(values: self.chart.points.map(\.value))
117+
let points = self.chart.points.enumerated().map { index, point in
118+
let x = self.chart.points.count == 1
119+
? size.width / 2
120+
: size.width * CGFloat(index) / CGFloat(self.chart.points.count - 1)
121+
let y = size.height * (1 - CGFloat(scale.fraction(for: point.value)))
122+
return CGPoint(x: x, y: y)
123+
}
124+
guard let first = points.first else { return }
125+
var path = Path()
126+
path.move(to: first)
127+
for point in points.dropFirst() {
128+
path.addLine(to: point)
129+
}
130+
context.stroke(
131+
path,
132+
with: .color(self.isHighlighted ? .white.opacity(0.82) : self.color),
133+
style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
134+
for point in points {
135+
context.fill(
136+
Path(ellipseIn: CGRect(x: point.x - 1.5, y: point.y - 1.5, width: 3, height: 3)),
137+
with: .color(self.isHighlighted ? .white : self.color))
138+
}
139+
}
140+
.overlay(alignment: .bottomLeading) {
141+
Rectangle()
142+
.fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(0.22))
143+
.frame(height: 1)
144+
}
145+
}
146+
}
147+
148+
private var accessibilityLabel: String {
149+
let title = self.chart.title.map { "\($0): " } ?? ""
150+
let unit = self.chart.unit.map { " \($0)" } ?? ""
151+
let points = self.chart.points.map { "\($0.label) \($0.value)\(unit)" }.joined(separator: ", ")
152+
return title + points
153+
}
154+
155+
private func fillColor(value: Double, scale: UsageChartScale) -> Color {
156+
let ratio = max(0.18, scale.fraction(for: value))
157+
if self.isHighlighted {
158+
return Color.white.opacity(0.55 + ratio * 0.35)
159+
}
160+
return self.color.opacity(0.42 + ratio * 0.58)
161+
}
162+
163+
private static func height(value: Double, scale: UsageChartScale, available: CGFloat) -> CGFloat {
164+
let ratio = scale.fraction(for: value)
165+
guard ratio > 0 else { return 1 }
166+
return max(3, CGFloat(ratio) * available)
167+
}
168+
}

0 commit comments

Comments
 (0)