Skip to content
5 changes: 5 additions & 0 deletions Scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ check_codex_parser_hash() {
"${ROOT_DIR}/Scripts/regenerate-codex-parser-hash.sh" --check
}

check_provider_manifests() {
"${ROOT_DIR}/Scripts/regenerate-provider-manifests.sh" --check
}

check_package_product_paths() {
"${ROOT_DIR}/Scripts/test_package_product_paths.sh"
}
Expand Down Expand Up @@ -85,6 +89,7 @@ check_llms_index() {

run_portable_checks() {
check_codex_parser_hash
check_provider_manifests
check_package_product_paths
check_package_strip
check_package_signing
Expand Down
247 changes: 247 additions & 0 deletions Scripts/regenerate-provider-manifests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROVIDERS_FILE="${ROOT_DIR}/Sources/CodexBarCore/Providers/Providers.swift"
DESCRIPTOR_DIR="${ROOT_DIR}/Sources/CodexBarCore/Providers"
IMPLEMENTATION_DIR="${ROOT_DIR}/Sources/CodexBar/Providers"
DESCRIPTOR_OUTPUT="${DESCRIPTOR_DIR}/ProviderManifest.swift"
IMPLEMENTATION_OUTPUT="${IMPLEMENTATION_DIR}/Shared/ProviderImplementationManifest.swift"
INSTANCE_ID_OUTPUT="${DESCRIPTOR_DIR}/ProviderInstanceIDAliases.generated.swift"
MODE="${1:-write}"

case "$MODE" in
write | --write)
CHECK_ONLY=0
;;
check | --check)
CHECK_ONLY=1
;;
*)
echo "Usage: $0 [write|--write|check|--check]" >&2
exit 2
;;
esac

# Canonical first-party bootstrap order. This single declaration generates both manifests and the
# ProviderInstanceID aliases; provider types are discovered from their provider-owned declarations.
PROVIDERS=(
codex
openai
azureopenai
claude
clinepass
cursor
opencode
opencodego
alibaba
alibabatokenplan
qwencloud
factory
gemini
antigravity
copilot
devin
zai
minimax
manus
kimi
kilo
kiro
vertexai
augment
jetbrains
moonshot
amp
t3chat
ollama
synthetic
openrouter
elevenlabs
warp
windsurf
zed
perplexity
mimo
doubao
sakana
abacus
mistral
deepseek
deepinfra
codebuff
crof
venice
commandcode
qoder
stepfun
bedrock
grok
groq
llmproxy
litellm
deepgram
poe
chutes
neuralwatt
clawrouter
longcat
sub2api
wayfinder
zenmux
aiand
zoommate
xai
notion
)

ENUM_PROVIDERS=()
while IFS= read -r provider; do
ENUM_PROVIDERS+=("$provider")
done < <(sed -nE '/public enum UsageProvider:/,/^}/s/^[[:space:]]*case ([a-z0-9]+)$/\1/p' "$PROVIDERS_FILE")

contains_provider() {
local needle="$1"
shift
local candidate
for candidate in "$@"; do
[[ "$candidate" == "$needle" ]] && return 0
done
return 1
}

for provider in "${ENUM_PROVIDERS[@]}"; do
if ! contains_provider "$provider" "${PROVIDERS[@]}"; then
echo "error: UsageProvider '${provider}' is missing from the canonical PROVIDERS order in ${BASH_SOURCE[0]#${ROOT_DIR}/}" >&2
exit 1
fi
done
for provider in "${PROVIDERS[@]}"; do
if ! contains_provider "$provider" "${ENUM_PROVIDERS[@]}"; then
echo "error: canonical provider '${provider}' has no UsageProvider case in ${PROVIDERS_FILE#${ROOT_DIR}/}" >&2
exit 1
fi
done

DESCRIPTOR_TYPES=()
IMPLEMENTATION_TYPES=()
for provider in "${PROVIDERS[@]}"; do
descriptor_matches=()
while IFS= read -r match; do
[[ -n "$match" ]] && descriptor_matches+=("$match")
done < <(grep -rlE "id: \\.${provider}," --include='*ProviderDescriptor.swift' "$DESCRIPTOR_DIR" || true)
if [[ "${#descriptor_matches[@]}" -ne 1 ]]; then
echo "error: provider '${provider}' must have exactly one descriptor file declaring 'id: .${provider},'; found ${#descriptor_matches[@]}" >&2
exit 1
fi
descriptor_type="$(sed -nE 's/^public enum ([A-Za-z0-9]+ProviderDescriptor).*/\1/p' "${descriptor_matches[0]}" | head -n 1)"
if [[ -z "$descriptor_type" ]]; then
echo "error: could not find the public ProviderDescriptor type for provider '${provider}' in ${descriptor_matches[0]#${ROOT_DIR}/}" >&2
exit 1
fi
DESCRIPTOR_TYPES+=("$descriptor_type")

implementation_matches=()
while IFS= read -r match; do
[[ -n "$match" ]] && implementation_matches+=("$match")
done < <(grep -rlE "let id: UsageProvider = \\.${provider}\$" --include='*ProviderImplementation.swift' "$IMPLEMENTATION_DIR" || true)
if [[ "${#implementation_matches[@]}" -ne 1 ]]; then
echo "error: provider '${provider}' must have exactly one implementation file declaring 'let id: UsageProvider = .${provider}'; found ${#implementation_matches[@]}" >&2
exit 1
fi
implementation_type="$(sed -nE 's/^(struct|final class) ([A-Za-z0-9]+ProviderImplementation).*/\2/p' "${implementation_matches[0]}" | head -n 1)"
if [[ -z "$implementation_type" ]]; then
echo "error: could not find the ProviderImplementation type for provider '${provider}' in ${implementation_matches[0]#${ROOT_DIR}/}" >&2
exit 1
fi
IMPLEMENTATION_TYPES+=("$implementation_type")
done

render_descriptors() {
cat <<'SWIFT'
// Generated by Scripts/regenerate-provider-manifests.sh. Do not edit by hand.
import Foundation

/// Closed first-party bootstrap list. Runtime/plugin registrations remain owned by
/// `ProviderDescriptorRegistry.register(_:)`.
public enum ProviderManifest {
public static let allDescriptors: [ProviderDescriptor] = [
SWIFT
for descriptor_type in "${DESCRIPTOR_TYPES[@]}"; do
printf ' %s.descriptor,\n' "$descriptor_type"
done
cat <<'SWIFT'
]
}
SWIFT
}

render_implementations() {
cat <<'SWIFT'
// Generated by Scripts/regenerate-provider-manifests.sh. Do not edit by hand.
import CodexBarCore
import Foundation

/// Closed first-party bootstrap list. Runtime/plugin registrations remain owned by
/// `ProviderImplementationRegistry.register(_:)`.
enum ProviderImplementationManifest {
static let makeImplementations: [@Sendable () -> any ProviderImplementation] = [
SWIFT
for implementation_type in "${IMPLEMENTATION_TYPES[@]}"; do
printf ' { %s() },\n' "$implementation_type"
done
cat <<'SWIFT'
]
}
SWIFT
}

render_instance_ids() {
cat <<'SWIFT'
// Generated by Scripts/regenerate-provider-manifests.sh. Do not edit by hand.

// First-party spellings keep call sites concise while their values remain validated instance IDs.
// swiftformat:disable sortDeclarations
extension ProviderInstanceID {
SWIFT
for provider in "${PROVIDERS[@]}"; do
printf ' public static let %s = UsageProvider.%s.instanceID\n' "$provider" "$provider"
done
cat <<'SWIFT'
}

// swiftformat:enable sortDeclarations
SWIFT
}

check_or_write() {
local output="$1"
local renderer="$2"
local expected
expected="$(mktemp)"
"$renderer" >"$expected"
if [[ "$CHECK_ONLY" -eq 1 ]]; then
if ! cmp -s "$expected" "$output"; then
echo "error: ${output#${ROOT_DIR}/} is stale. Run Scripts/regenerate-provider-manifests.sh and commit the result." >&2
if [[ -f "$output" ]]; then
diff -u "$output" "$expected" >&2 || true
else
diff -u /dev/null "$expected" >&2 || true
fi
rm -f "$expected"
return 1
fi
rm -f "$expected"
return 0
fi
mv "$expected" "$output"
echo "Updated ${output#${ROOT_DIR}/}"
}

check_or_write "$DESCRIPTOR_OUTPUT" render_descriptors
check_or_write "$IMPLEMENTATION_OUTPUT" render_implementations
check_or_write "$INSTANCE_ID_OUTPUT" render_instance_ids
if [[ "$CHECK_ONLY" -eq 1 ]]; then
echo "Provider manifests are current (${#PROVIDERS[@]} providers)"
fi
21 changes: 6 additions & 15 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,21 +268,12 @@ extension UsageMenuCardView.Model {
}

static func tokenUsageHintLines(provider: UsageProvider) -> [String] {
switch provider {
case .codex:
[L("codex_api_estimate_hint")]
case .claude, .cursor:
[UsageFormatter.costEstimateHint(provider: provider)]
case .vertexai:
[L("cost_estimate_hint")]
case .bedrock:
[L("AWS Cost Explorer billing can lag.")]
case .openai:
[L("Reported by OpenAI Admin API organization usage.")]
case .mistral:
[L("Reported by Mistral billing usage.")]
default:
[]
ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.menuHintLines.map { hint in
switch hint {
case let .localized(key): L(key)
case .estimate: UsageFormatter.costEstimateHint(provider: provider)
case let .literal(text): L(text)
}
}
}

Expand Down
47 changes: 30 additions & 17 deletions Sources/CodexBar/PreferencesDebugPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,10 @@ struct DebugPane: View {
caption: L("probe_logs_caption"))
{
Picker(L("Provider"), selection: self.$currentLogProvider) {
Text("Codex").tag(UsageProvider.codex)
Text("Claude").tag(UsageProvider.claude)
Text("Cursor").tag(UsageProvider.cursor)
Text("Augment").tag(UsageProvider.augment)
Text("Amp").tag(UsageProvider.amp)
Text("Ollama").tag(UsageProvider.ollama)
ForEach(Self.probeLogProviders, id: \.self) { provider in
Text(ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName)
.tag(provider)
}
}
.pickerStyle(.segmented)
.frame(width: 460)
Expand Down Expand Up @@ -223,6 +221,7 @@ struct DebugPane: View {
title: L("section_caches"),
caption: L("caches_caption"))
{
// Provider-specific by design: cache activity currently tracks the Codex/Claude scanners.
let isTokenRefreshActive = self.store.isTokenRefreshInFlight(for: .codex)
|| self.store.isTokenRefreshInFlight(for: .claude)

Expand Down Expand Up @@ -261,8 +260,10 @@ struct DebugPane: View {
caption: L("notifications_caption"))
{
Picker(L("Provider"), selection: self.$currentLogProvider) {
Text("Codex").tag(UsageProvider.codex)
Text("Claude").tag(UsageProvider.claude)
ForEach(Self.notificationSimulationProviders, id: \.self) { provider in
Text(ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName)
.tag(provider)
}
}
.pickerStyle(.segmented)
.frame(width: 240)
Expand Down Expand Up @@ -309,15 +310,10 @@ struct DebugPane: View {
caption: L("error_simulation_caption"))
{
Picker(L("Provider"), selection: self.$currentErrorProvider) {
Text("Codex").tag(UsageProvider.codex)
Text("Claude").tag(UsageProvider.claude)
Text("Gemini").tag(UsageProvider.gemini)
Text("Antigravity").tag(UsageProvider.antigravity)
Text("Augment").tag(UsageProvider.augment)
Text("Amp").tag(UsageProvider.amp)
Text("T3 Chat").tag(UsageProvider.t3chat)
Text("ZoomMate").tag(UsageProvider.zoommate)
Text("Ollama").tag(UsageProvider.ollama)
ForEach(Self.errorSimulationProviders, id: \.self) { provider in
Text(ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName)
.tag(provider)
}
}
.pickerStyle(.segmented)
.frame(width: 360)
Expand All @@ -343,6 +339,7 @@ struct DebugPane: View {
.controlSize(.small)
}

// Provider-specific by design: only Codex/Claude expose scanner-owned token errors here.
let supportsTokenError = self.currentErrorProvider == .codex || self.currentErrorProvider == .claude
HStack(spacing: 12) {
Button {
Expand Down Expand Up @@ -370,6 +367,7 @@ struct DebugPane: View {
title: L("section_cli_paths"),
caption: L("cli_paths_caption"))
{
// Provider-specific by design: PathDebugInfo only records the Codex and Claude binaries.
self.binaryRow(title: L("codex_binary"), value: self.store.pathDebugInfo.codexBinary)
self.binaryRow(title: L("claude_binary"), value: self.store.pathDebugInfo.claudeBinary)

Expand Down Expand Up @@ -419,6 +417,21 @@ struct DebugPane: View {
CodexBarLog.fileLogURL.path
}

private static let probeLogProviders = Self.providers { $0.probeLogOrder }
private static let notificationSimulationProviders = Self.providers { $0.notificationSimulationOrder }
#if DEBUG
private static let errorSimulationProviders = Self.providers { $0.errorSimulationOrder }
#endif

private static func providers(order: (ProviderDebugPaneCapabilities) -> Int?) -> [UsageProvider] {
ProviderDescriptorRegistry.all.compactMap { descriptor -> (UsageProvider, Int)? in
guard let rank = order(descriptor.metadata.debugPane) else { return nil }
return (descriptor.id, rank)
}
.sorted { $0.1 < $1.1 }
.map(\.0)
}

private var animationPatternBinding: Binding<LoadingPattern?> {
Binding(
get: { self.settings.debugLoadingPattern },
Expand Down
Loading