mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-23 15:19:37 +00:00
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
Adds xAI's Grok Bot desktop app as a provider: per-bot sessions with estimated usage, and the weekly allowance read through the Cursor IDE login on the CLI, desktop Plans screen and menubar. No row unless the app is installed. Two independent review rounds on real data.
216 lines
7.5 KiB
Swift
216 lines
7.5 KiB
Swift
import AppKit
|
|
|
|
/// Shared cache for provider glyphs. Curated vector resources are preferred;
|
|
/// existing CodeBurn raster artwork remains a compatibility fallback. Unknown
|
|
/// providers receive a crisp, neutral generated sigil rather than an invented
|
|
/// approximation of their trademark.
|
|
@MainActor
|
|
enum ProviderIconCache {
|
|
struct ResourceCandidate: Equatable {
|
|
let name: String
|
|
let fileExtension: String
|
|
}
|
|
|
|
struct SigilDescriptor: Equatable {
|
|
let monogram: String
|
|
let quarterTurns: Int
|
|
let reach: Int
|
|
let terminal: Int
|
|
}
|
|
|
|
private static var images: [String: NSImage] = [:]
|
|
|
|
/// Product-facing IDs that intentionally share a provider mark.
|
|
private static let bundledVectorAliases: [String: String] = [
|
|
"alibabatokenplan": "alibaba",
|
|
"azureopenai": "codex",
|
|
"codex": "codex",
|
|
"openai": "codex",
|
|
"githubcopilot": "copilot",
|
|
"grokbot": "grok",
|
|
"googlegemini": "gemini",
|
|
"moonshot": "kimi",
|
|
]
|
|
|
|
/// Names already shipped by CodeBurn. These are retained behind the SVGs
|
|
/// for compatibility with older packaged resources and non-catalog views.
|
|
private static let bundledRasterAliases: [String: String] = [
|
|
"codex": "openai",
|
|
"openai": "openai",
|
|
"azureopenai": "openai",
|
|
"claude": "claude",
|
|
"gemini": "googlegemini",
|
|
"googlegemini": "googlegemini",
|
|
"copilot": "githubcopilot",
|
|
"githubcopilot": "githubcopilot",
|
|
"kimi": "kimi",
|
|
"antigravity": "antigravity",
|
|
"devin": "devin",
|
|
"general": "general",
|
|
"about": "about",
|
|
"about-flame": "about-flame",
|
|
"flame": "flame",
|
|
"flame-solid": "flame-solid",
|
|
]
|
|
|
|
static func resourceCandidates(for name: String) -> [ResourceCandidate] {
|
|
let canonical = canonicalName(name)
|
|
var candidates: [ResourceCandidate] = []
|
|
|
|
let vectorName = bundledVectorAliases[canonical] ?? canonical
|
|
if ProviderConnectionCatalog.entry(id: canonical) != nil
|
|
|| bundledVectorAliases[canonical] != nil
|
|
{
|
|
candidates.append(ResourceCandidate(name: "provider-\(vectorName)", fileExtension: "svg"))
|
|
}
|
|
|
|
if let resource = bundledRasterAliases[canonical] {
|
|
candidates.append(ResourceCandidate(name: resource, fileExtension: "png"))
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
static func monogram(for name: String) -> String {
|
|
let words = name
|
|
.split(whereSeparator: { !$0.isLetter && !$0.isNumber })
|
|
.map(String.init)
|
|
if words.count > 1 {
|
|
return words.prefix(2).compactMap(\.first).map(String.init).joined().uppercased()
|
|
}
|
|
|
|
let letters = name.filter { $0.isLetter || $0.isNumber }
|
|
return String(letters.prefix(2)).uppercased()
|
|
}
|
|
|
|
static func sigilDescriptor(for name: String) -> SigilDescriptor {
|
|
let hash = stableHash(canonicalName(name))
|
|
return SigilDescriptor(
|
|
monogram: monogram(for: name),
|
|
quarterTurns: Int(hash & 0b11),
|
|
reach: 16 + Int((hash >> 2) % 9),
|
|
terminal: Int((hash >> 6) & 0b1)
|
|
)
|
|
}
|
|
|
|
static func image(named name: String) -> NSImage? {
|
|
let cacheKey = canonicalName(name)
|
|
if let cached = images[cacheKey] { return cached }
|
|
for candidate in resourceCandidates(for: name) {
|
|
for subdirectory in ["Resources/ProviderIcons", "ProviderIcons", nil] {
|
|
if let url = Bundle.module.url(
|
|
forResource: candidate.name,
|
|
withExtension: candidate.fileExtension,
|
|
subdirectory: subdirectory
|
|
), let image = NSImage(contentsOf: url) {
|
|
image.isTemplate = true
|
|
images[cacheKey] = image
|
|
return image
|
|
}
|
|
}
|
|
}
|
|
let image = generatedSigil(named: name)
|
|
images[cacheKey] = image
|
|
return image
|
|
}
|
|
|
|
static func generatedSigil(named name: String) -> NSImage {
|
|
let descriptor = sigilDescriptor(for: name)
|
|
let size = NSSize(width: 128, height: 128)
|
|
let image = NSImage(size: size, flipped: false) { rect in
|
|
drawSigil(descriptor, in: rect)
|
|
return true
|
|
}
|
|
image.isTemplate = true
|
|
return image
|
|
}
|
|
|
|
private static func canonicalName(_ name: String) -> String {
|
|
name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
}
|
|
|
|
/// FNV-1a gives the same provider a stable visual treatment across launches
|
|
/// without relying on Swift's intentionally randomized `Hasher`.
|
|
private static func stableHash(_ value: String) -> UInt64 {
|
|
var hash: UInt64 = 14_695_981_039_346_656_037
|
|
for byte in value.utf8 {
|
|
hash ^= UInt64(byte)
|
|
hash &*= 1_099_511_628_211
|
|
}
|
|
return hash
|
|
}
|
|
|
|
private static func drawSigil(_ descriptor: SigilDescriptor, in rect: NSRect) {
|
|
NSGraphicsContext.saveGraphicsState()
|
|
let rotation = NSAffineTransform()
|
|
rotation.translateX(by: rect.midX, yBy: rect.midY)
|
|
rotation.rotate(byDegrees: CGFloat(descriptor.quarterTurns * 90))
|
|
rotation.translateX(by: -rect.midX, yBy: -rect.midY)
|
|
rotation.concat()
|
|
|
|
NSColor.black.setStroke()
|
|
NSColor.black.setFill()
|
|
|
|
let reach = CGFloat(descriptor.reach)
|
|
let upper = NSBezierPath()
|
|
upper.move(to: NSPoint(x: 25, y: 63))
|
|
upper.line(to: NSPoint(x: 25, y: 82))
|
|
upper.curve(
|
|
to: NSPoint(x: 46, y: 103),
|
|
controlPoint1: NSPoint(x: 25, y: 95),
|
|
controlPoint2: NSPoint(x: 33, y: 103)
|
|
)
|
|
upper.line(to: NSPoint(x: 46 + reach, y: 103))
|
|
upper.lineWidth = 8
|
|
upper.lineCapStyle = .round
|
|
upper.lineJoinStyle = .round
|
|
upper.stroke()
|
|
|
|
let lower = NSBezierPath()
|
|
lower.move(to: NSPoint(x: 103, y: 65))
|
|
lower.line(to: NSPoint(x: 103, y: 46))
|
|
lower.curve(
|
|
to: NSPoint(x: 82, y: 25),
|
|
controlPoint1: NSPoint(x: 103, y: 33),
|
|
controlPoint2: NSPoint(x: 95, y: 25)
|
|
)
|
|
lower.line(to: NSPoint(x: 82 - reach, y: 25))
|
|
lower.lineWidth = 8
|
|
lower.lineCapStyle = .round
|
|
lower.lineJoinStyle = .round
|
|
lower.stroke()
|
|
|
|
let terminalCenter = descriptor.terminal == 0
|
|
? NSPoint(x: 25, y: 63)
|
|
: NSPoint(x: 103, y: 65)
|
|
NSBezierPath(
|
|
ovalIn: NSRect(
|
|
x: terminalCenter.x - 5,
|
|
y: terminalCenter.y - 5,
|
|
width: 10,
|
|
height: 10
|
|
)
|
|
).fill()
|
|
NSGraphicsContext.restoreGraphicsState()
|
|
|
|
let paragraph = NSMutableParagraphStyle()
|
|
paragraph.alignment = .center
|
|
let text = (descriptor.monogram.isEmpty ? "?" : descriptor.monogram) as NSString
|
|
let fontSize: CGFloat = text.length == 1 ? 47 : 39
|
|
let attributes: [NSAttributedString.Key: Any] = [
|
|
.font: NSFont.systemFont(ofSize: fontSize, weight: .bold),
|
|
.foregroundColor: NSColor.black,
|
|
.paragraphStyle: paragraph,
|
|
.kern: -1.2,
|
|
]
|
|
let textSize = text.size(withAttributes: attributes)
|
|
text.draw(
|
|
in: NSRect(
|
|
x: rect.minX,
|
|
y: rect.midY - textSize.height / 2,
|
|
width: rect.width,
|
|
height: textSize.height
|
|
),
|
|
withAttributes: attributes
|
|
)
|
|
}
|
|
}
|