BroV: single Mac target, chat brain = local claude CLI (stream-json, resume)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 19:49:32 +03:00
parent 53b4f60da1
commit 0f77ec8704
25 changed files with 471 additions and 1007 deletions
+26 -2
View File
@@ -89,9 +89,23 @@ final class AppState: ObservableObject {
}
// In-chat provider + model — picked via the model selector in the prompt view
@Published var chatProvider: ChatProvider = .anthropic {
@Published var chatProvider: ChatProvider = .claudeCode {
didSet { UserDefaults.standard.set(chatProvider.rawValue, forKey: "chatProvider") }
}
// Claude Code CLI chat (BroV): model alias ("" = CLI default), folder it runs in,
// optional path to the binary, and the permission mode passed to `claude`.
@Published var claudeCodeModel: String = "" {
didSet { UserDefaults.standard.set(claudeCodeModel, forKey: "claudeCodeModel") }
}
@Published var chatWorkingDirectory: String = "~" {
didSet { UserDefaults.standard.set(chatWorkingDirectory, forKey: "chatWorkingDirectory") }
}
@Published var claudeBinaryPath: String = "" {
didSet { UserDefaults.standard.set(claudeBinaryPath, forKey: "claudeBinaryPath") }
}
@Published var claudePermissionMode: String = "default" {
didSet { UserDefaults.standard.set(claudePermissionMode, forKey: "claudePermissionMode") }
}
@Published var googleChatModel: String = ChatProvider.google.defaultModel {
didSet { UserDefaults.standard.set(googleChatModel, forKey: "googleChatModel") }
}
@@ -126,6 +140,10 @@ final class AppState: ObservableObject {
func fetchModelsIfNeeded(for provider: ChatProvider) {
guard !loadingProviderModels.contains(provider),
fetchedProviderModels[provider] == nil else { return }
if provider == .claudeCode {
fetchedProviderModels[provider] = ClaudeCodeCLI.models
return
}
// Local providers: fetch from server URL (no API key needed)
if provider.isLocal {
let baseURL = provider == .ollama ? ollamaServerURL : lmstudioServerURL
@@ -170,6 +188,7 @@ final class AppState: ObservableObject {
Task {
let models: [(id: String, label: String)]
switch provider {
case .claudeCode: models = ClaudeCodeCLI.models
case .anthropic: models = await ClaudeService.fetchModels(apiKey: apiKey)
case .google: models = await ClaudeService.fetchGoogleModels(apiKey: apiKey)
case .openai: models = await ClaudeService.fetchOpenAIModels(apiKey: apiKey)
@@ -193,7 +212,7 @@ final class AppState: ObservableObject {
if !models.contains(where: { $0.id == openAIChatModel }) {
openAIChatModel = models.first(where: { $0.id.contains("mini") })?.id ?? models.first!.id
}
case .ollama, .lmstudio: break
case .ollama, .lmstudio, .claudeCode: break
}
}
}
@@ -202,6 +221,7 @@ final class AppState: ObservableObject {
/// The model currently active for chat (provider-aware).
var activeChatModel: String {
switch chatProvider {
case .claudeCode: return claudeCodeModel
case .anthropic: return claudeModel
case .google: return googleChatModel
case .openai: return openAIChatModel
@@ -409,6 +429,10 @@ final class AppState: ObservableObject {
if let v = ud.string(forKey: "claudeModel"),
!v.trimmingCharacters(in: .whitespaces).isEmpty { claudeModel = v }
if let v = ud.string(forKey: "chatProvider"), let p = ChatProvider(rawValue: v) { chatProvider = p }
if let v = ud.string(forKey: "claudeCodeModel") { claudeCodeModel = v }
if let v = ud.string(forKey: "chatWorkingDirectory"), !v.isEmpty { chatWorkingDirectory = v }
if let v = ud.string(forKey: "claudeBinaryPath") { claudeBinaryPath = v }
if let v = ud.string(forKey: "claudePermissionMode"), !v.isEmpty { claudePermissionMode = v }
if let v = ud.string(forKey: "googleChatModel"), !v.isEmpty { googleChatModel = v }
if let v = ud.string(forKey: "openAIChatModel"), !v.isEmpty { openAIChatModel = v }
if let v = ud.string(forKey: "ollamaChatModel"), !v.isEmpty { ollamaChatModel = v }