swift-apple-intelligence-grpc/Sources/AppleIntelligenceApp/ServerManager.swift
Mathias Beaulieu-Duncan e0bf17da3d 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>
2025-12-30 04:31:31 -05:00

124 lines
3.3 KiB
Swift

import Foundation
import AppleIntelligenceCore
import GRPCCore
import GRPCNIOTransportHTTP2
@MainActor
@Observable
final class ServerManager {
enum ServerState: Equatable {
case stopped
case starting
case running(host: String, port: Int)
case error(String)
var isRunning: Bool {
if case .running = self { return true }
return false
}
var statusText: String {
switch self {
case .stopped:
return "Server offline"
case .starting:
return "Starting..."
case .running(let host, let port):
return "Running on \(host):\(port)"
case .error:
return "Server offline"
}
}
}
private(set) var state: ServerState = .stopped
private(set) var modelStatus: String = "Unknown"
private var serverTask: Task<Void, Never>?
private var service: AppleIntelligenceService?
private let settings: AppSettings
init(settings: AppSettings) {
self.settings = settings
}
func start() {
guard !state.isRunning else { return }
state = .starting
// Capture settings values for use in Task
let host = settings.host
let port = settings.port
let apiKey = settings.apiKey.isEmpty ? nil : settings.apiKey
serverTask = Task {
do {
// Initialize Apple Intelligence service
let aiService = await AppleIntelligenceService()
self.service = aiService
let isAvailable = await aiService.isAvailable
let status = await aiService.getModelStatus()
await MainActor.run {
self.modelStatus = status
}
guard isAvailable else {
await MainActor.run {
self.state = .error("Apple Intelligence not available")
}
return
}
// Create provider
let provider = AppleIntelligenceProvider(service: aiService, apiKey: apiKey)
// Create transport and server
let transport = HTTP2ServerTransport.Posix(
address: .ipv4(host: host, port: port),
transportSecurity: .plaintext,
config: .defaults
)
let server = GRPCServer(transport: transport, services: [provider])
await MainActor.run {
self.state = .running(host: host, port: port)
}
// Run server until cancelled
try await server.serve()
} catch is CancellationError {
// Normal shutdown
} catch {
await MainActor.run {
self.state = .error(error.localizedDescription)
}
}
await MainActor.run {
if case .running = self.state {
self.state = .stopped
}
}
}
}
func stop() {
serverTask?.cancel()
serverTask = nil
state = .stopped
}
func toggle() {
if state.isRunning {
stop()
} else {
start()
}
}
}