swift-apple-intelligence-grpc/Sources/AppleIntelligenceApp/ServerManager.swift
Mathias Beaulieu-Duncan 638656e7ca Add vision support, gRPC reflection toggle, and chat improvements
- Add Vision framework integration for image analysis (OCR, classification)
- Add image attachment support in chat UI with drag & drop
- Add recent images sidebar from Downloads/Desktop
- Add copy to clipboard button for assistant responses
- Add gRPC reflection service with toggle in settings
- Create proper .proto file and generate Swift code
- Add server restart when toggling reflection setting
- Fix port number formatting in settings (remove comma grouping)
- Update gRPC dependencies to v2.x

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 16:18:06 -05:00

148 lines
4.2 KiB
Swift

import Foundation
import AppleIntelligenceCore
import GRPCCore
import GRPCNIOTransportHTTP2
import GRPCReflectionService
@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
let enableReflection = settings.enableReflection
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
)
// Build services list with optional reflection
var services: [any RegistrableRPCService] = [provider]
if enableReflection {
if let descriptorURL = AppleIntelligenceResources.descriptorSetURL {
let reflectionService = try ReflectionService(descriptorSetFileURLs: [descriptorURL])
services.append(reflectionService)
}
}
let server = GRPCServer(transport: transport, services: services)
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 restart() {
guard state.isRunning else { return }
// Stop the current server
stop()
state = .starting
// Start again after a short delay to allow port release
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.start()
}
}
func toggle() {
if state.isRunning {
stop()
} else {
start()
}
}
}