Skip to content
Merged
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
8 changes: 8 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ SwiftUI windows at 960, 1180 and 1440 logical points, captured at the display's
2x backing scale. Credit balance uses the existing secondary-text treatment and
stays separate from reset grants, dollar amounts and local history.

The allowance grouping pass uses direction A with the owner's constraint to make
few changes from the current design. The two existing provider cards keep their
headings, percentage meters and controls. Labelled credit and reset-grant groups
sit below the meters; source/check details use native disclosure. Claude grant
counts stay distinct from scheduled resets, with an explicit unreported state
and a link to Claude Usage when the CLI supplies no count. Native full-window
evidence covers 960, 1180 and 1440 points, including missing and stale readings.

## Selected direction

Owner-directed StorageDaddy family fork: keep the A+C information architecture (live operations plus a policy ledger), but express it through StorageDaddy's warm black-and-mint visual language, rounded typography, fine outlines, and editorial Daddy doodles.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The primary navigation is **Usage**, **Skills**, **Projects**, and **OpenTelemet
- Usage first scan shows side-by-side Codex/Claude allowance followed by one local history chart including Devin. The historical chart has shared agent filters, model/model-provider/project grouping where supported, range, day/week/month scale, generated/cache/cost metrics where available, selectable periods and exact breakdown. Model, project, and session drill-downs remain below. Cache reads, generated tokens, and estimated cost stay separate; unpriced models are flagged.
- Skills can run an explicit, read-only **Read local history** scan. Claude and Devin `skill` tool calls are counted separately from Cursor/Grok skill-file reads and Codex tool calls that reference a `SKILL.md` path. The library shows last-seen dates, agent and folder evidence, 30/90-day or all-history windows, and an evidence-only filter. Cursor dates use transcript modification time because its records lack event timestamps. Name-only tool calls cannot identify a particular same-name copy; no recorded event never means unused. This backfill is held in memory for the app session and does not automatically remove skills.
- Project grouping is a separately labelled session ledger, joining ccusage session IDs to Codex's read-only thread-index `cwd`, Grok's project paths, and Claude's encoded project slugs. It queries only Codex rollout path and working directory metadata, never prompt bodies. Buckets use session last activity, not an invented daily allocation; totals may not reconcile to daily accounting. Missing session identity remains **Unattributed**. Model-provider labels are inferred from reported model names, not billing endpoints; unknown aliases remain unknown. Devin's indexed daily tokens participate in the shared model and provider grouping; project attribution remains unavailable.
- Provider allowance for Codex and Claude is fetched on **Check both allowances**, or on opening Usage after the user enables the opt-in automatic switch (at most once per 15 minutes). It is never added to local token history. Codex's reported credit balance appears in credit units, separately from full-reset grants; unlimited, unavailable, and unreported balances remain explicit. Codex reset-credit expiry is shown only when reported; detail rows may be capped. Claude's available usage-reset count is shown when its CLI emits a limit-reset notice; absent counts are explicitly not reported, never assumed to be zero. These grants are separate from paid usage credits and may cover only particular limits. ContextDaddy only reads the notices and never invokes `/limit-reset` or consumes a grant.
- Provider allowance for Codex and Claude is fetched on **Check both allowances**, or on opening Usage after the user enables the opt-in automatic switch (at most once per 15 minutes). It is never added to local token history. Codex's reported credit balance appears in credit units, separately from full-reset grants; unlimited, unavailable, and unreported balances remain explicit. Codex reset-credit expiry is shown only when reported; detail rows may be capped. Claude's available usage-reset count is shown in the labelled Reset grants section when its CLI emits a limit-reset notice; absent counts are explicitly not reported, never assumed to be zero, with a direct link to Claude Settings > Usage. Scheduled reset times stay with their allowance meters, and Source & reading details discloses the provider source and full check time. These grants are separate from paid usage credits and may cover only particular limits. ContextDaddy only reads the notices and never invokes `/limit-reset` or consumes a grant.
- Skills and OTEL review panels can **Copy all issues** into an agent-ready brief with IDs, evidence, source scope, and verification limits. After skill edits, **Verify after changes** rescans and distinguishes detector-cleared, still-detected, and unverified findings. The library supports previewed local skill edits with recovery; OTEL signals need a new comparable observation window.
- Files & diagnostics configuration findings have the same copy-all and rescan handoff without copying configuration values or modifying files. This file audit detects the two misplaced `otel.*` keys but cannot detect launch-time `session-flags.token_budget`; that warning must be traced to the launcher supplying the flag.
- Cursor remains inventory-only for usage until a verified source exists.
Expand Down
116 changes: 75 additions & 41 deletions Sources/ContextDaddy/UsageSourcePanels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,53 +64,72 @@ struct UsageAllowanceView: View {
.foregroundStyle(DaddyTheme.muted)
}
Spacer()
Text(status == nil ? "NOT CHECKED" : status?.status == "ready"
Text(error != nil ? (status == nil ? "UNAVAILABLE" : "STALE") : status == nil ? "NOT CHECKED" : status?.status == "ready"
? (windows.isEmpty ? "READY" : health(worst).0.uppercased()) : "UNAVAILABLE")
.font(.caption2.weight(.bold))
.foregroundStyle(status?.status == "ready" && error == nil ? health(worst).1 : DaddyTheme.muted)
.foregroundStyle(error != nil ? DaddyTheme.amber : status?.status == "ready" ? health(worst).1 : DaddyTheme.muted)
}
if status?.status == "ready" {
ViewThatFits(in: .horizontal) {
HStack(alignment: .top, spacing: 14) {
ForEach(windows, id: \.id) { window in
windowValue(window).frame(maxWidth: .infinity, alignment: .leading)
}
}
VStack(alignment: .leading, spacing: 12) {
ForEach(windows, id: \.id) { window in windowValue(window) }
HStack(alignment: .top, spacing: 14) {
ForEach(windows, id: \.id) { window in
windowValue(window).frame(maxWidth: .infinity, alignment: .leading)
}
}
if let status, let creditText = Self.creditText(status) {
Text(creditText).font(.caption2.weight(.semibold))
.foregroundStyle(DaddyTheme.muted)
.fixedSize(horizontal: false, vertical: true)
if let resets = status.resetCredits {
Text(resetCountText(resets, provider: provider))
.font(.caption2).foregroundStyle(DaddyTheme.muted)
if let status {
Divider().overlay(DaddyTheme.line)
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
Text(provider == "claude" ? "PAID USAGE CREDITS" : "CREDIT BALANCE")
.font(.caption2.weight(.bold)).foregroundStyle(DaddyTheme.muted)
Text(Self.creditText(status) ?? "Not reported")
.font(.callout.weight(.semibold)).monospacedDigit()
.fixedSize(horizontal: false, vertical: true)
}.frame(maxWidth: .infinity, alignment: .leading)
VStack(alignment: .leading, spacing: 6) {
Text("RESET GRANTS")
.font(.caption2.weight(.bold)).foregroundStyle(DaddyTheme.muted)
Text(Self.resetCountText(status))
.font(.callout.weight(.semibold)).monospacedDigit()
.fixedSize(horizontal: false, vertical: true)
if provider == "claude", status.resetCredits == nil {
Text("Claude Code did not report a count.")
.font(.caption2).foregroundStyle(DaddyTheme.muted)
claudeUsageLink
}
if provider == "codex", status.resetCredits != nil {
resetExpiry(status)
}
}.frame(maxWidth: .infinity, alignment: .leading)
}
} else if let resets = status?.resetCredits {
Text(resetCountText(resets, provider: provider))
.font(.caption2).foregroundStyle(DaddyTheme.muted)
}
if provider == "claude", status?.resetCredits == nil {
Text("Usage reset count not reported")
.font(.caption2).foregroundStyle(DaddyTheme.muted)
}
if let status, provider == "codex", status.resetCredits != nil {
resetExpiry(status)
}
} else {
Text(status?.message ?? "Check both allowances for an account reading.")
.font(.caption).foregroundStyle(DaddyTheme.muted)
.fixedSize(horizontal: false, vertical: true)
if provider == "claude", error != nil { claudeUsageLink }
}
if let error {
Text("Latest check failed; any saved reading may be stale. \(error)")
.font(.caption2).foregroundStyle(DaddyTheme.amber)
}
if let status {
Text("\(status.source) · \(status.checkedAt)")
.font(.caption2).foregroundStyle(DaddyTheme.muted).lineLimit(1)
Divider().overlay(DaddyTheme.line)
DisclosureGroup("Source & reading details") {
VStack(alignment: .leading, spacing: 5) {
Text(status.source)
Text("Checked · \(status.checkedAt)")
if provider == "codex",
let nonExpiring = status.resetCreditsWithoutExpiryCount, nonExpiring > 0,
status.latestReportedResetCreditExpiryUnix != nil {
Text("\(nonExpiring) reported \(nonExpiring == 1 ? "grant has" : "grants have") no expiry.")
}
}
.font(.caption2).foregroundStyle(DaddyTheme.muted)
.fixedSize(horizontal: false, vertical: true)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 5)
}.font(.caption).foregroundStyle(DaddyTheme.muted)
}
}.frame(maxWidth: .infinity, alignment: .leading)
}
Expand Down Expand Up @@ -145,13 +164,14 @@ struct UsageAllowanceView: View {
if let pace = paceLabel(window) {
Text(pace).font(.caption2.monospaced()).foregroundStyle(DaddyTheme.muted)
}
if let reset = window.resetDescription {
Text("Resets \(reset)").font(.caption2.monospaced()).foregroundStyle(DaddyTheme.muted)
if let reset = Self.windowResetText(window) {
Text(reset).font(.caption2.monospaced()).foregroundStyle(DaddyTheme.muted)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(minWidth: 0, maxWidth: 190, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel("\(window.label), \(Int(window.remainingPercent.rounded())) percent remaining")
.accessibilityLabel("\(window.label), \(Int(window.remainingPercent.rounded())) percent remaining. \(Self.windowResetText(window) ?? "")")
}

private func health(_ remaining: Double) -> (String, Color) {
Expand Down Expand Up @@ -194,8 +214,27 @@ struct UsageAllowanceView: View {
return nil
}

private func resetCountText(_ count: UInt64, provider: String) -> String {
"\(count) \(provider == "claude" ? "usage" : "full") \(count == 1 ? "reset" : "resets") available"
static let claudeUsageURL = URL(string: "https://claude.ai/settings/usage")!

private var claudeUsageLink: some View {
Link(destination: Self.claudeUsageURL) {
HStack(spacing: 3) {
Text("View Claude Usage")
Image(systemName: "arrow.up.right")
}.font(.caption2)
}
.buttonStyle(.plain).foregroundStyle(DaddyTheme.mint)
}

static func resetCountText(_ status: ProviderQuotaStatus) -> String {
guard let count = status.resetCredits else { return "Not reported" }
return "\(count) \(status.provider == "claude" ? "usage" : "full") \(count == 1 ? "reset" : "resets") available"
}

static func windowResetText(_ window: ProviderQuotaWindow) -> String? {
if let description = window.resetDescription { return "Resets \(description)" }
guard let unix = window.resetsAtUnix else { return nil }
return "Resets \(Date(timeIntervalSince1970: TimeInterval(unix)).formatted(date: .abbreviated, time: .shortened))"
}

@ViewBuilder private func resetExpiry(_ status: ProviderQuotaStatus) -> some View {
Expand All @@ -204,16 +243,11 @@ struct UsageAllowanceView: View {
.font(.caption2.weight(.semibold)).foregroundStyle(DaddyTheme.muted)
} else if (status.resetCredits ?? 0) > 0 {
Text(status.resetCreditsWithoutExpiryCount == status.resetCredits
? "Reported credits do not expire" : "Reset-credit expiry unavailable")
.font(.caption2).foregroundStyle(DaddyTheme.muted)
}
if let nonExpiring = status.resetCreditsWithoutExpiryCount, nonExpiring > 0,
status.latestReportedResetCreditExpiryUnix != nil {
Text("\(nonExpiring) reported \(nonExpiring == 1 ? "credit has" : "credits have") no expiry.")
? "Reported reset grants do not expire" : "Reset-grant expiry unavailable")
.font(.caption2).foregroundStyle(DaddyTheme.muted)
}
if let details = status.resetCreditDetailsCount, details < (status.resetCredits ?? 0) {
Text("Only \(details) of \(status.resetCredits ?? 0) credit details reported; a later expiry may exist.")
Text("Only \(details) of \(status.resetCredits ?? 0) grant details reported; a later expiry may exist.")
.font(.caption2).foregroundStyle(DaddyTheme.amber)
}
}
Expand Down
Loading
Loading