codeburn/mac/Sources/CodeBurnMenubar/Data/MenubarPayload.swift
Resham Joshi 495a254338 feat(mac): native Swift menubar app + one-command install
Introduces mac/ with a native SwiftUI menubar app that replaces the
previous SwiftBar plugin entirely. Install via `npx codeburn menubar`,
which downloads the .app from GitHub Releases, strips Gatekeeper
quarantine, and drops it into ~/Applications.

Highlights

- mac/ SwiftUI app: agent tabs, Today/7/30/Month/All period switcher,
  Trend/Forecast/Pulse/Stats/Plan insights, activity + model
  breakdowns, optimize findings, CSV/JSON export, Star-on-GitHub
  banner, live 60s refresh, instant currency switching with offline FX
  cache.
- Security: CodeburnCLI argv-based spawn (no shell interpretation),
  SafeFile symlink guards + O_NOFOLLOW writes, FX rate clamping to
  [0.0001, 1_000_000], keychain filtered to account == "default",
  removed byte-window credential log, in-flight refresh guard, POSIX
  flock on config.json writes, TerminalLauncher validates argv before
  AppleScript interpolation.
- Performance: shared static NumberFormatter (thousands of allocations
  per popover redraw eliminated), concurrent pipe drain with 20 MB cap
  + 60s timeout in DataClient, Observation-tracked reactive UI, 5-min
  payload cache keyed on (period, provider).
- CLI: new `codeburn menubar` subcommand that downloads + installs +
  launches the .app (no clone, no build). New `status --format
  menubar-json` payload builder. `export` rewritten to produce a
  folder of one-table-per-file CSVs with a `.codeburn-export` marker
  so arbitrary -o paths cannot be silently deleted.
- Removed: src/menubar.ts (SwiftBar plugin generator),
  install-menubar / uninstall-menubar subcommands, `status --format
  menubar` directive output, tests/menubar.test.ts,
  tests/security/menubar-injection.test.ts.
- Release: .github/workflows/release-menubar.yml builds universal
  binary, assembles .app, ad-hoc signs, zips, uploads on mac-v* tag
  push. Runs on the free macos-latest runner.

Tests

- 230 TypeScript tests pass
- 10 Swift CapacityEstimator tests pass
- TypeScript typecheck clean
- Swift release build clean
2026-04-17 16:55:56 -07:00

123 lines
3.6 KiB
Swift

import Foundation
/// Shape of `codeburn status --format menubar-json --period <period>`.
/// `current` is scoped to the requested period; the whole payload reflects that slice.
struct MenubarPayload: Codable, Sendable {
let generated: String
let current: CurrentBlock
let optimize: OptimizeBlock
let history: HistoryBlock
}
struct HistoryBlock: Codable, Sendable {
let daily: [DailyHistoryEntry]
}
struct DailyModelBreakdown: Codable, Sendable {
let name: String
let cost: Double
let calls: Int
let inputTokens: Int
let outputTokens: Int
var totalTokens: Int { inputTokens + outputTokens }
}
struct DailyHistoryEntry: Codable, Sendable {
let date: String
let cost: Double
let calls: Int
let inputTokens: Int
let outputTokens: Int
let cacheReadTokens: Int
let cacheWriteTokens: Int
let topModels: [DailyModelBreakdown]
/// Pricing-ratio prior: input + 5x output + cache_creation + 0.1x cache_read.
/// Matches Anthropic's published per-token pricing on Sonnet/Opus closely enough to be a useful proxy.
var effectiveTokens: Double {
Double(inputTokens) + 5.0 * Double(outputTokens) + Double(cacheWriteTokens) + 0.1 * Double(cacheReadTokens)
}
}
extension DailyHistoryEntry {
/// Required for legacy payloads (no topModels emitted yet).
enum CodingKeys: String, CodingKey {
case date, cost, calls, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, topModels
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
date = try c.decode(String.self, forKey: .date)
cost = try c.decode(Double.self, forKey: .cost)
calls = try c.decode(Int.self, forKey: .calls)
inputTokens = try c.decode(Int.self, forKey: .inputTokens)
outputTokens = try c.decode(Int.self, forKey: .outputTokens)
cacheReadTokens = try c.decode(Int.self, forKey: .cacheReadTokens)
cacheWriteTokens = try c.decode(Int.self, forKey: .cacheWriteTokens)
topModels = try c.decodeIfPresent([DailyModelBreakdown].self, forKey: .topModels) ?? []
}
}
struct CurrentBlock: Codable, Sendable {
let label: String
let cost: Double
let calls: Int
let sessions: Int
let oneShotRate: Double?
let inputTokens: Int
let outputTokens: Int
let cacheHitPercent: Double
let topActivities: [ActivityEntry]
let topModels: [ModelEntry]
let providers: [String: Double]
}
struct ActivityEntry: Codable, Sendable {
let name: String
let cost: Double
let turns: Int
let oneShotRate: Double?
}
struct ModelEntry: Codable, Sendable {
let name: String
let cost: Double
let calls: Int
}
struct OptimizeBlock: Codable, Sendable {
let findingCount: Int
let savingsUSD: Double
let topFindings: [FindingEntry]
}
struct FindingEntry: Codable, Sendable {
let title: String
let impact: String
let savingsUSD: Double
}
// MARK: - Empty fallback
extension MenubarPayload {
/// Strictly-empty payload. Used as the fallback before real data arrives, so no
/// plausible-looking fake numbers leak into the UI.
static let empty = MenubarPayload(
generated: "",
current: CurrentBlock(
label: "",
cost: 0,
calls: 0,
sessions: 0,
oneShotRate: nil,
inputTokens: 0,
outputTokens: 0,
cacheHitPercent: 0,
topActivities: [],
topModels: [],
providers: [:]
),
optimize: OptimizeBlock(findingCount: 0, savingsUSD: 0, topFindings: []),
history: HistoryBlock(daily: [])
)
}