vibetunnel/mac/VibeTunnel/Core/Services/TerminalControlHandler.swift

154 lines
5.8 KiB
Swift

import Foundation
import OSLog
/// Handles terminal control messages via the unified control socket
@MainActor
final class TerminalControlHandler {
private let logger = Logger(subsystem: BundleIdentifiers.loggerSubsystem, category: "TerminalControl")
// MARK: - Singleton
static let shared = TerminalControlHandler()
// MARK: - Initialization
private init() {
// Register handler with the shared socket manager
// NOTE: System handlers (like SystemControlHandler) need to be registered separately
// since they may have different lifecycle requirements
SharedUnixSocketManager.shared.registerControlHandler(for: .terminal) { [weak self] data in
await self?.handleMessage(data)
}
logger.info("🚀 Terminal control handler initialized")
}
// MARK: - Message Handling
private func handleMessage(_ data: Data) async -> Data? {
do {
// First check what action this is
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let action = json["action"] as? String
{
switch action {
case "spawn":
// Try to decode as terminal spawn request
if let spawnRequest = try? ControlProtocol.decodeTerminalSpawnRequest(data) {
logger
.info(
"📥 Terminal spawn request for session: \(spawnRequest.payload?.sessionId ?? "unknown")"
)
let response = await handleSpawnRequest(spawnRequest)
return try ControlProtocol.encode(response)
} else {
logger.error("Failed to decode terminal spawn request")
return createErrorResponse(for: data, error: "Invalid spawn request format")
}
default:
logger.error("Unknown terminal action: \(action)")
return createErrorResponse(for: data, error: "Unknown terminal action: \(action)")
}
} else {
logger.error("Invalid terminal message format")
return createErrorResponse(for: data, error: "Invalid message format")
}
} catch {
logger.error("Failed to process terminal message: \(error)")
return createErrorResponse(for: data, error: "Failed to process message: \(error.localizedDescription)")
}
}
private func handleSpawnRequest(_ message: ControlProtocol.TerminalSpawnRequestMessage) async -> ControlProtocol
.TerminalSpawnResponseMessage
{
guard let payload = message.payload else {
return ControlProtocol.terminalSpawnResponse(
to: message,
success: false,
error: "Missing payload"
)
}
logger.info("Spawning terminal session \(payload.sessionId)")
do {
// If a specific terminal is requested, temporarily set it
var originalTerminal: String?
if let requestedTerminal = payload.terminalPreference {
originalTerminal = UserDefaults.standard.string(forKey: "preferredTerminal")
UserDefaults.standard.set(requestedTerminal, forKey: "preferredTerminal")
}
defer {
// Restore original terminal preference if we changed it
if let original = originalTerminal {
UserDefaults.standard.set(original, forKey: "preferredTerminal")
}
}
// Launch the terminal
try TerminalLauncher.shared.launchOptimizedTerminalSession(
workingDirectory: payload.workingDirectory ?? "",
command: payload.command ?? "",
sessionId: payload.sessionId,
vibetunnelPath: nil // Use bundled path
)
// Success response with compile-time guarantees
return ControlProtocol.terminalSpawnResponse(
to: message,
success: true
)
} catch {
logger.error("Failed to spawn terminal: \(error)")
return ControlProtocol.terminalSpawnResponse(
to: message,
success: false,
error: error.localizedDescription
)
}
}
// MARK: - Public Methods
/// Start the terminal control handler
func start() {
// Handler is registered in init, just log that we're ready
logger.info("✅ Terminal control handler started")
}
/// Stop the terminal control handler
func stop() {
SharedUnixSocketManager.shared.unregisterControlHandler(for: .terminal)
logger.info("🛑 Terminal control handler stopped")
}
// MARK: - Error Handling
private func createErrorResponse(for data: Data, error: String) -> Data? {
do {
// Try to get request ID for proper error response
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let id = json["id"] as? String,
let action = json["action"] as? String
{
// Create error response matching request
let errorResponse: [String: Any] = [
"id": id,
"type": "response",
"category": "terminal",
"action": action,
"error": error
]
return try JSONSerialization.data(withJSONObject: errorResponse)
}
} catch {
logger.error("Failed to create error response: \(error)")
}
return nil
}
}