mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-26 08:39:39 +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
The catalog comparison matched specifier lists, so a key whose placeholders are all %@ passed even when the Chinese value bound the arguments into the wrong slots — the drift that shipped in three early-reset keys and was only caught by rendering real data bilingually (#1331). All 42 multi-argument keys in both tables now use positional specifiers (%1$@, %2$lld, ...); the parity check compares slot to verb so Chinese word order stays free while each slot keeps its type; a new test fails on any multi-argument key that is not positional. The source scanner closes its three expression gaps: ternary branches, concatenated literals, and let-bound literals passed by name. Text(verbatim:) stays the explicit opt-out; the four provider-name fallbacks the new rules surfaced in HeatmapSection route through the catalog as verbatim entries. Closes #1331
116 lines
5.6 KiB
Swift
116 lines
5.6 KiB
Swift
import Foundation
|
|
|
|
/// Pure display-decision helpers for the Copilot quota surfaces.
|
|
///
|
|
/// Copilot tokens are read straight from whichever signed-in client already
|
|
/// holds one, with no refresh path, so a revoked token sits in
|
|
/// `.terminalFailure` until the user signs in again. Like Kimi and
|
|
/// Gemini, the always-visible surfaces keep showing the last good snapshot
|
|
/// with a quiet caption instead of flapping to a reconnect screen; the
|
|
/// reconnect screen is reserved for the no-data case, where there is
|
|
/// genuinely nothing to show.
|
|
///
|
|
/// Explicit Disconnect is different from missing credentials: the store keeps
|
|
/// `.notBootstrapped` while credentials stay on disk. Plan, Settings, and
|
|
/// Capacity Dock nil-quota copy therefore take the persisted opt-out flag and
|
|
/// must not claim the token is gone.
|
|
enum CopilotQuotaPresentation {
|
|
/// Which Plan-tab subview to render, given the load state and whether a
|
|
/// last-known snapshot exists.
|
|
enum PlanContent: Equatable {
|
|
case noCredentials
|
|
/// Explicit Disconnect: quota tracking is off, credentials untouched.
|
|
case disconnected
|
|
case loading
|
|
case failed
|
|
case transientFailed
|
|
case reconnect(reason: String?)
|
|
/// Render the loaded usage bars. `idle` is true when the login has
|
|
/// gone terminal but a snapshot is still on hand — the caller stamps a
|
|
/// quiet "sign in again" caption instead of hiding the data.
|
|
case usage(idle: Bool)
|
|
}
|
|
|
|
static var noCredentialsPlanTitle: String { L("No Copilot credentials found") }
|
|
static var noCredentialsPlanMessage: String {
|
|
L("Sign in via an editor's Copilot plugin first. Then click Try Again.")
|
|
}
|
|
static var disconnectedPlanTitle: String { L("Copilot quota tracking disconnected") }
|
|
static var disconnectedPlanMessage: String {
|
|
L("Your Copilot credentials are untouched. Click Connect to resume.")
|
|
}
|
|
static var noCredentialsSettingsDetail: String {
|
|
L("Usage tracking still works. For live quota, sign in with the Copilot CLI or gh auth login, or paste a token below, then click Connect.")
|
|
}
|
|
static var disconnectedSettingsDetail: String {
|
|
L("Quota tracking disconnected. Credentials are untouched. Click Connect to resume.")
|
|
}
|
|
|
|
static func planContent(
|
|
loadState: SubscriptionLoadState,
|
|
hasUsage: Bool,
|
|
explicitlyDisconnected: Bool = false
|
|
) -> PlanContent {
|
|
switch loadState {
|
|
case .notBootstrapped:
|
|
return explicitlyDisconnected ? .disconnected : .noCredentials
|
|
case .noCredentials:
|
|
return .noCredentials
|
|
case .dormant, .bootstrapping:
|
|
return .loading
|
|
case .loading, .loaded:
|
|
return hasUsage ? .usage(idle: false) : .loading
|
|
case .failed:
|
|
return .failed
|
|
case .transientFailure:
|
|
return hasUsage ? .usage(idle: false) : .transientFailed
|
|
case .terminalFailure(let reason):
|
|
return hasUsage ? .usage(idle: true) : .reconnect(reason: reason)
|
|
}
|
|
}
|
|
|
|
/// Settings connection-row detail for a loaded snapshot. It always names
|
|
/// the host that answered, so a GitHub Enterprise Cloud tenant can see its
|
|
/// own `api.<tenant>.ghe.com` endpoint rather than a dotcom claim (#1286).
|
|
static func connectedSettingsDetail(plan: String?, apiHost: String) -> String {
|
|
let host = apiHost.isEmpty ? CopilotHostEndpoint.defaultAPIHost : apiHost
|
|
// The host is an API hostname and the plan comes from GitHub; both are
|
|
// substituted verbatim, only the sentence around them is translated.
|
|
guard let plan, !plan.isEmpty else { return L("Live quota tracked from %@.", host) }
|
|
return L("Plan: %1$@. Live quota tracked from %2$@.", plan, host)
|
|
}
|
|
|
|
static func settingsNotConnectedDetail(explicitlyDisconnected: Bool) -> String {
|
|
explicitlyDisconnected ? disconnectedSettingsDetail : noCredentialsSettingsDetail
|
|
}
|
|
|
|
/// Settings connection-row detail before anything has been fetched. It
|
|
/// used to promise `api.github.com`, which is wrong for every enterprise
|
|
/// tenant now that each rung can carry its own host (#1306): with no
|
|
/// answer yet there is no host to name, so it names one only when a
|
|
/// previous snapshot already proved which host answers.
|
|
static func dormantSettingsDetail(apiHost: String?) -> String {
|
|
guard let apiHost, !apiHost.isEmpty, apiHost != CopilotHostEndpoint.defaultAPIHost else {
|
|
return L("Tap Load Quota to fetch live usage from GitHub.")
|
|
}
|
|
return L("Tap Load Quota to fetch live usage from %@.", apiHost)
|
|
}
|
|
|
|
/// Why a host typed next to the pasted token cannot be used, or nil when
|
|
/// it can. Rejecting it in Settings turns what would otherwise be a
|
|
/// terminal fetch failure into an answer at the field the user typed —
|
|
/// and keeps an unaddressable host out of the Keychain record entirely.
|
|
static func pastedHostRejection(_ raw: String) -> String? {
|
|
let host = CopilotHostEndpoint.normalize(raw) ?? CopilotHostEndpoint.defaultHost
|
|
guard CopilotHostEndpoint.apiHost(for: host) == nil else { return nil }
|
|
return L("CodeBurn cannot read Copilot quota for %@. Use github.com or a GitHub Enterprise Cloud host (*.ghe.com).", host)
|
|
}
|
|
|
|
/// Snapshot age past which a loaded view stamps an "as of <time>" caption,
|
|
/// so a bar can never silently masquerade as current.
|
|
static let stalenessThreshold: TimeInterval = 10 * 60
|
|
|
|
static func isStale(fetchedAt: Date, now: Date = Date()) -> Bool {
|
|
now.timeIntervalSince(fetchedAt) > stalenessThreshold
|
|
}
|
|
}
|