Add OpenClaw, Roo Code, and KiloCode providers (#175)

- OpenClaw: JSONL parser with multi-path discovery, tool extraction
  (toolCall + tool_use block types), model tracking via model_change
  and custom model-snapshot events
- Roo Code + KiloCode: shared Cline-family parser extracts model from
  <model> tags in api_conversation_history.json, strips provider
  prefixes from model names
- Add cline-auto and openclaw-auto aliases and display names
- Add menubar provider filters and tab colors for all three
- Show cached data instantly instead of blocking on CLI refresh
This commit is contained in:
Resham Joshi 2026-04-28 09:24:14 -07:00 committed by GitHub
parent ce78ac52c1
commit ec2de6a642
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1034 additions and 7 deletions

View file

@ -65,13 +65,13 @@ final class AppStore {
/// Switch to a period. Always fetches fresh data so the user never sees stale numbers.
func switchTo(period: Period) async {
selectedPeriod = period
await refresh(includeOptimize: true)
await refresh(includeOptimize: true, force: true)
}
/// Switch to a provider filter. Always fetches fresh data so the user never sees stale numbers.
func switchTo(provider: ProviderFilter) async {
selectedProvider = provider
await refresh(includeOptimize: true)
await refresh(includeOptimize: true, force: true)
}
private var inFlightKeys: Set<PayloadCacheKey> = []
@ -79,11 +79,15 @@ final class AppStore {
/// Refresh the currently selected (period, provider) combination. Guards against concurrent
/// fetches for the same key so a slow initial request can't overwrite a newer one that
/// finished first (which would show stale numbers the user has already moved past).
func refresh(includeOptimize: Bool) async {
/// When `force` is false (background timer), skips the CLI call if the cache is still fresh.
func refresh(includeOptimize: Bool, force: Bool = false) async {
let key = currentKey
if !force, cache[key]?.isFresh == true { return }
guard !inFlightKeys.contains(key) else { return }
inFlightKeys.insert(key)
isLoading = true
if cache[key] == nil {
isLoading = true
}
defer {
inFlightKeys.remove(key)
isLoading = false
@ -228,15 +232,21 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
case copilot = "Copilot"
case gemini = "Gemini"
case kiro = "Kiro"
case kiloCode = "KiloCode"
case openclaw = "OpenClaw"
case opencode = "OpenCode"
case pi = "Pi"
case omp = "OMP"
case rooCode = "Roo Code"
var id: String { rawValue }
var providerKeys: [String] {
switch self {
case .cursor: ["cursor", "cursor agent"]
case .rooCode: ["roo-code"]
case .kiloCode: ["kilo-code"]
case .openclaw: ["openclaw"]
default: [rawValue.lowercased()]
}
}
@ -249,10 +259,13 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
case .cursor: "cursor"
case .copilot: "copilot"
case .gemini: "gemini"
case .kiloCode: "kilo-code"
case .kiro: "kiro"
case .openclaw: "openclaw"
case .opencode: "opencode"
case .pi: "pi"
case .omp: "omp"
case .rooCode: "roo-code"
}
}
}

View file

@ -93,10 +93,13 @@ extension ProviderFilter {
case .cursor: return Theme.categoricalCursor
case .copilot: return Color(red: 0x6D/255.0, green: 0x8F/255.0, blue: 0xA6/255.0)
case .gemini: return Color(red: 0x44/255.0, green: 0x85/255.0, blue: 0xF4/255.0)
case .kiloCode: return Color(red: 0x00/255.0, green: 0x96/255.0, blue: 0x88/255.0)
case .kiro: return Color(red: 0x4A/255.0, green: 0x9E/255.0, blue: 0xC4/255.0)
case .openclaw: return Color(red: 0xDA/255.0, green: 0x70/255.0, blue: 0x56/255.0)
case .opencode: return Color(red: 0x5B/255.0, green: 0x83/255.0, blue: 0x5B/255.0)
case .pi: return Color(red: 0xB2/255.0, green: 0x6B/255.0, blue: 0x3D/255.0)
case .omp: return Color(red: 0x8B/255.0, green: 0x5C/255.0, blue: 0xB0/255.0)
case .rooCode: return Color(red: 0x4C/255.0, green: 0xAF/255.0, blue: 0x50/255.0)
}
}
}

View file

@ -397,7 +397,7 @@ struct FooterBar: View {
.fixedSize()
Button {
Task { await store.refresh(includeOptimize: true) }
Task { await store.refresh(includeOptimize: true, force: true) }
} label: {
Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise")
.font(.system(size: 11, weight: .medium))