Files
clawdbot/apps/macos/Sources/Clawdis/ControlRequestHandler.swift
2025-12-13 12:29:39 +00:00

213 lines
9.5 KiB
Swift

import ClawdisIPC
import Foundation
import OSLog
enum ControlRequestHandler {
static func process(
request: Request,
notifier: NotificationManager = NotificationManager(),
logger: Logger = Logger(subsystem: "com.steipete.clawdis", category: "control")) async throws -> Response
{
// Keep `status` responsive even if the main actor is busy.
let paused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
if paused {
switch request {
case .status:
break
default:
return Response(ok: false, message: "clawdis paused")
}
}
switch request {
case let .notify(title, body, sound, priority, delivery):
let chosenSound = sound?.trimmingCharacters(in: .whitespacesAndNewlines)
let chosenDelivery = delivery ?? .system
switch chosenDelivery {
case .system:
let ok = await notifier.send(title: title, body: body, sound: chosenSound, priority: priority)
return ok ? Response(ok: true) : Response(ok: false, message: "notification not authorized")
case .overlay:
await MainActor.run {
NotifyOverlayController.shared.present(title: title, body: body)
}
return Response(ok: true)
case .auto:
let ok = await notifier.send(title: title, body: body, sound: chosenSound, priority: priority)
if ok { return Response(ok: true) }
await MainActor.run {
NotifyOverlayController.shared.present(title: title, body: body)
}
return Response(ok: true, message: "notification not authorized; used overlay")
}
case let .ensurePermissions(caps, interactive):
let statuses = await PermissionManager.ensure(caps, interactive: interactive)
let missing = statuses.filter { !$0.value }.map(\.key.rawValue)
let ok = missing.isEmpty
let msg = ok ? "all granted" : "missing: \(missing.joined(separator: ","))"
return Response(ok: ok, message: msg)
case .status:
return paused ? Response(ok: false, message: "clawdis paused") : Response(ok: true, message: "ready")
case .rpcStatus:
let result = await AgentRPC.shared.status()
return Response(ok: result.ok, message: result.error)
case .uiListScreens:
let screens = await MainActor.run { UIScreenService.listScreens() }
let payload = try JSONEncoder().encode(screens)
return Response(ok: true, payload: payload)
case let .uiScreenshot(screenIndex, windowID):
let authorized = await PermissionManager
.ensure([.screenRecording], interactive: false)[.screenRecording] ?? false
guard authorized else { return Response(ok: false, message: "screen recording permission missing") }
let resolution: (screenIndex: Int?, displayID: UInt32?) = await Task { @MainActor in
if let screenIndex,
let match = UIScreenService.listScreens().first(where: { $0.index == screenIndex })
{
return (screenIndex, match.displayID)
}
return (nil, nil)
}.value
let data = await Task { @MainActor in
await Screenshotter.capture(displayID: resolution.displayID, windowID: windowID)
}.value
guard let data else {
return Response(ok: false, message: "screenshot failed")
}
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("clawdis-ui", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let outURL = dir.appendingPathComponent("screenshot-\(Int(Date().timeIntervalSince1970 * 1000)).png")
do {
try data.write(to: outURL)
} catch {
return Response(ok: false, message: "failed to write screenshot: \(error.localizedDescription)")
}
let size = ScreenshotSize.readPNGSize(data: data)
let result = UIScreenshotResult(
path: outURL.path,
width: size?.width ?? 0,
height: size?.height ?? 0,
screenIndex: resolution.screenIndex,
displayID: resolution.displayID,
windowID: windowID)
let payload = try JSONEncoder().encode(result)
return Response(ok: true, payload: payload)
case let .runShell(command, cwd, env, timeoutSec, needsSR):
if needsSR {
let authorized = await PermissionManager
.ensure([.screenRecording], interactive: false)[.screenRecording] ?? false
guard authorized else { return Response(ok: false, message: "screen recording permission missing") }
}
return await ShellExecutor.run(command: command, cwd: cwd, env: env, timeout: timeoutSec)
case let .agent(message, thinking, session, deliver, to):
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return Response(ok: false, message: "message empty") }
let sessionKey = session ?? "main"
let rpcResult = await AgentRPC.shared.send(
text: trimmed,
thinking: thinking,
sessionKey: sessionKey,
deliver: deliver,
to: to,
channel: nil)
return rpcResult.ok
? Response(ok: true, message: rpcResult.text ?? "sent")
: Response(ok: false, message: rpcResult.error ?? "failed to send")
case let .canvasShow(session, path, placement):
let canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
guard canvasEnabled else {
return Response(ok: false, message: "Canvas disabled by user")
}
do {
let dir = try await MainActor.run { try CanvasManager.shared.show(
sessionKey: session,
path: path,
placement: placement) }
return Response(ok: true, message: dir)
} catch {
return Response(ok: false, message: error.localizedDescription)
}
case let .canvasHide(session):
await MainActor.run { CanvasManager.shared.hide(sessionKey: session) }
return Response(ok: true)
case let .canvasGoto(session, path, placement):
let canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
guard canvasEnabled else {
return Response(ok: false, message: "Canvas disabled by user")
}
do {
try await MainActor.run { try CanvasManager.shared.goto(
sessionKey: session,
path: path,
placement: placement) }
return Response(ok: true)
} catch {
return Response(ok: false, message: error.localizedDescription)
}
case let .canvasEval(session, javaScript):
let canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
guard canvasEnabled else {
return Response(ok: false, message: "Canvas disabled by user")
}
do {
let result = try await CanvasManager.shared.eval(sessionKey: session, javaScript: javaScript)
return Response(ok: true, payload: Data(result.utf8))
} catch {
return Response(ok: false, message: error.localizedDescription)
}
case let .canvasSnapshot(session, outPath):
let canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
guard canvasEnabled else {
return Response(ok: false, message: "Canvas disabled by user")
}
do {
let path = try await CanvasManager.shared.snapshot(sessionKey: session, outPath: outPath)
return Response(ok: true, message: path)
} catch {
return Response(ok: false, message: error.localizedDescription)
}
case .nodeList:
let ids = await BridgeServer.shared.connectedNodeIds()
let payload = (try? JSONSerialization.data(
withJSONObject: ["connectedNodeIds": ids],
options: [.prettyPrinted]))
.flatMap { String(data: $0, encoding: .utf8) }
?? "{}"
return Response(ok: true, payload: Data(payload.utf8))
case let .nodeInvoke(nodeId, command, paramsJSON):
do {
let res = try await BridgeServer.shared.invoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON)
if res.ok {
let payload = res.payloadJSON ?? ""
return Response(ok: true, payload: Data(payload.utf8))
}
let errText = res.error?.message ?? "node invoke failed"
return Response(ok: false, message: errText)
} catch {
return Response(ok: false, message: error.localizedDescription)
}
}
}
}