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
+65 -1
View File
@@ -217,6 +217,10 @@ final class ClaudeService {
// MARK: - Chat (multi-turn, natural text + web search)
func chat(query: String, context: PromptContext?, state: AppState) async {
if state.chatProvider == .claudeCode {
await chatClaudeCode(query: query, context: context, state: state)
return
}
guard state.chatProvider == .anthropic else {
await chatOpenAICompatible(query: query, context: context, state: state)
return
@@ -264,6 +268,66 @@ final class ClaudeService {
}
}
// MARK: - Claude Code CLI chat (BroV: the user's own `claude`, no API key)
func chatClaudeCode(query: String, context: PromptContext?, state: AppState) async {
let cli = ClaudeCodeCLI.shared
var prompt = query
// Context goes with the first message of a conversation, like the API chat.
if cli.sessionID == nil, let context {
switch context {
case .window(let app, let title, let url):
var prefix = "Context — App: \(app), Window: \(title)"
if let url { prefix += ", URL: \(url)" }
prompt = prefix + "\n\n" + query
case .file(let name, let fileURL):
// The CLI reads files itself: hand it the path.
if let fileURL {
prompt = "Attached file: \(fileURL.path)\n(read it before answering)\n\n" + query
} else {
prompt = "File: \(name)\n\n" + query
}
}
}
let placeholder = ChatMessage(role: .assistant, content: "")
let msgId = placeholder.id
state.chatHistory.append(placeholder)
state.stateOverride = .thinking
do {
let final = try await cli.send(
prompt: prompt,
model: state.claudeCodeModel,
workingDirectory: state.chatWorkingDirectory,
binaryPath: state.claudeBinaryPath,
permissionMode: state.claudePermissionMode
) { [state, msgId] visible in
if !visible.isEmpty, state.stateOverride == .thinking {
state.stateOverride = nil // hide typing dots on first visible text
}
if let idx = state.chatHistory.firstIndex(where: { $0.id == msgId }) {
state.chatHistory[idx].content = visible
}
}
let trimmed = final.trimmingCharacters(in: .whitespacesAndNewlines)
if let idx = state.chatHistory.firstIndex(where: { $0.id == msgId }) {
if trimmed.isEmpty { state.chatHistory.remove(at: idx) }
else { state.chatHistory[idx].content = trimmed }
}
state.stateOverride = nil
state.view = .prompt
NotificationCenter.default.post(name: .triggerEmote, object: BotEmote.happy)
} catch is CancellationError {
state.chatHistory.removeAll { $0.id == msgId && $0.content.isEmpty }
state.stateOverride = nil
} catch {
state.chatHistory.removeAll { $0.id == msgId && $0.content.isEmpty }
state.stateOverride = nil
await showError(error.localizedDescription, state: state)
}
}
// MARK: - OpenAI-compatible chat (Google Gemini / OpenAI / Ollama / LM Studio)
func chatOpenAICompatible(query: String, context: PromptContext?, state: AppState) async {
@@ -279,7 +343,7 @@ final class ClaudeService {
switch provider {
case .google: baseURL = "https://generativelanguage.googleapis.com/v1beta/openai"
case .openai: baseURL = "https://api.openai.com/v1"
case .anthropic, .ollama, .lmstudio: baseURL = ""
case .anthropic, .ollama, .lmstudio, .claudeCode: baseURL = ""
}
}