// Sources/Core/Sessions/SessionRegistry.swift // T-2.6: Session listing, creation, and deletion via the sidecar REST API. import Foundation // MARK: - SessionInfo public struct SessionInfo: Identifiable, Hashable, Sendable { public let id: String public let name: String public let state: String // "running", "idle", etc. } // MARK: - SessionRegistry @MainActor public final class SessionRegistry: ObservableObject { @Published public var sessions: [SessionInfo] = [] @Published public var isLoading = false @Published public var error: String? = nil public init() {} // MARK: - Private helpers private func baseURL(credential: SidecarCredential) -> URL { URL(string: "http://\(credential.host):\(credential.port)")! } private func authorizedRequest(url: URL, credential: SidecarCredential) -> URLRequest { var req = URLRequest(url: url) req.setValue("Bearer \(credential.bearerToken)", forHTTPHeaderField: "Authorization") return req } // MARK: - GET /sessions /// Fetches the current session list from the sidecar and updates `sessions`. public func refresh(credential: SidecarCredential) async { isLoading = true error = nil defer { isLoading = false } let url = baseURL(credential: credential).appendingPathComponent("sessions") let req = authorizedRequest(url: url, credential: credential) do { let (data, _) = try await URLSession.shared.data(for: req) let items = try JSONDecoder().decode([SessionItem].self, from: data) sessions = items.map { SessionInfo(id: $0.id, name: $0.name, state: $0.state) } } catch { self.error = error.localizedDescription } } // MARK: - POST /sessions /// Creates a new session with the given name and returns the created `SessionInfo`. @discardableResult public func spawnSession(name: String, credential: SidecarCredential) async throws -> SessionInfo { let url = baseURL(credential: credential).appendingPathComponent("sessions") var req = authorizedRequest(url: url, credential: credential) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONEncoder().encode(["name": name]) let (data, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw SessionRegistryError.unexpectedStatus } let item = try JSONDecoder().decode(SessionItem.self, from: data) let info = SessionInfo(id: item.id, name: item.name, state: item.state) // Append optimistically; a follow-up refresh will normalise order. sessions.append(info) return info } // MARK: - DELETE /sessions/ /// Deletes the session with the given id. public func deleteSession(id: String, credential: SidecarCredential) async throws { let url = baseURL(credential: credential) .appendingPathComponent("sessions") .appendingPathComponent(id) var req = authorizedRequest(url: url, credential: credential) req.httpMethod = "DELETE" let (_, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw SessionRegistryError.unexpectedStatus } sessions.removeAll { $0.id == id } } } // MARK: - Private Codable helper private struct SessionItem: Decodable { let id: String let name: String let state: String } // MARK: - Errors public enum SessionRegistryError: LocalizedError { case unexpectedStatus public var errorDescription: String? { switch self { case .unexpectedStatus: return "Unexpected HTTP status from sidecar." } } }