Add macOS menu bar app with chat and settings

- Restructure project into three targets:
  - AppleIntelligenceCore: Shared gRPC service code
  - AppleIntelligenceServer: CLI server
  - AppleIntelligenceApp: Menu bar app

- Menu bar app features:
  - Toggle server on/off from menu bar
  - Chat window with streaming AI responses
  - Settings: host, port, API key, auto-start, launch at login
  - Proper window focus handling for menu bar apps

- Add build scripts for distribution:
  - build-app.sh: Creates signed .app bundle
  - create-dmg.sh: Creates distributable DMG

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mathias Beaulieu-Duncan
2025-12-30 04:31:31 -05:00
parent 5279565797
commit e0bf17da3d
17 changed files with 855 additions and 21 deletions
@@ -0,0 +1,52 @@
import Foundation
import ServiceManagement
@Observable
final class AppSettings {
var host: String {
didSet { UserDefaults.standard.set(host, forKey: "grpc_host") }
}
var port: Int {
didSet { UserDefaults.standard.set(port, forKey: "grpc_port") }
}
var apiKey: String {
didSet { UserDefaults.standard.set(apiKey, forKey: "api_key") }
}
var autoStartServer: Bool {
didSet { UserDefaults.standard.set(autoStartServer, forKey: "auto_start_server") }
}
var launchAtLogin: Bool {
didSet {
do {
if launchAtLogin {
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
print("Failed to update launch at login: \(error)")
}
}
}
init() {
self.host = UserDefaults.standard.string(forKey: "grpc_host") ?? "0.0.0.0"
let savedPort = UserDefaults.standard.integer(forKey: "grpc_port")
self.port = savedPort == 0 ? 50051 : savedPort
self.apiKey = UserDefaults.standard.string(forKey: "api_key") ?? ""
self.autoStartServer = UserDefaults.standard.bool(forKey: "auto_start_server")
self.launchAtLogin = SMAppService.mainApp.status == .enabled
}
func resetToDefaults() {
host = "0.0.0.0"
port = 50051
apiKey = ""
autoStartServer = false
launchAtLogin = false
}
}
@@ -0,0 +1,22 @@
import Foundation
struct ChatMessage: Identifiable, Equatable {
let id: UUID
let role: Role
var content: String
let timestamp: Date
var isStreaming: Bool
enum Role: Equatable {
case user
case assistant
}
init(role: Role, content: String, isStreaming: Bool = false) {
self.id = UUID()
self.role = role
self.content = content
self.timestamp = Date()
self.isStreaming = isStreaming
}
}