fix(macos): restore control + webchat build

main
Peter Steinberger 2025-12-13 19:38:35 +00:00
parent e2a93e17f9
commit 39c232548c
2 changed files with 107 additions and 114 deletions

View File

@ -10,89 +10,87 @@ enum ControlRequestHandler {
{ {
// Keep `status` responsive even if the main actor is busy. // Keep `status` responsive even if the main actor is busy.
let paused = UserDefaults.standard.bool(forKey: pauseDefaultsKey) let paused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
if paused, request != .status { if paused, case .status = request {
// allow status through
} else if paused {
return Response(ok: false, message: "clawdis paused") return Response(ok: false, message: "clawdis paused")
} }
switch request { switch request {
case let .notify(title, body, sound, priority, delivery): case let .notify(title, body, sound, priority, delivery):
let notify = NotifyRequest( let notify = NotifyRequest(
title: title, title: title,
body: body, body: body,
sound: sound, sound: sound,
priority: priority, priority: priority,
delivery: delivery delivery: delivery)
) return await self.handleNotify(notify, notifier: notifier)
return await self.handleNotify(notify, notifier: notifier)
case let .ensurePermissions(caps, interactive): case let .ensurePermissions(caps, interactive):
return await self.handleEnsurePermissions(caps: caps, interactive: interactive) return await self.handleEnsurePermissions(caps: caps, interactive: interactive)
case .status: case .status:
return paused return paused
? Response(ok: false, message: "clawdis paused") ? Response(ok: false, message: "clawdis paused")
: Response(ok: true, message: "ready") : Response(ok: true, message: "ready")
case .rpcStatus: case .rpcStatus:
return await self.handleRPCStatus() return await self.handleRPCStatus()
case let .runShell(command, cwd, env, timeoutSec, needsSR): case let .runShell(command, cwd, env, timeoutSec, needsSR):
return await self.handleRunShell( return await self.handleRunShell(
command: command, command: command,
cwd: cwd, cwd: cwd,
env: env, env: env,
timeoutSec: timeoutSec, timeoutSec: timeoutSec,
needsSR: needsSR needsSR: needsSR)
)
case let .agent(message, thinking, session, deliver, to): case let .agent(message, thinking, session, deliver, to):
return await self.handleAgent( return await self.handleAgent(
message: message, message: message,
thinking: thinking, thinking: thinking,
session: session, session: session,
deliver: deliver, deliver: deliver,
to: to to: to)
)
case let .canvasShow(session, path, placement): case let .canvasShow(session, path, placement):
return await self.handleCanvasShow(session: session, path: path, placement: placement) return await self.handleCanvasShow(session: session, path: path, placement: placement)
case let .canvasHide(session): case let .canvasHide(session):
return await self.handleCanvasHide(session: session) return await self.handleCanvasHide(session: session)
case let .canvasGoto(session, path, placement): case let .canvasGoto(session, path, placement):
return await self.handleCanvasGoto(session: session, path: path, placement: placement) return await self.handleCanvasGoto(session: session, path: path, placement: placement)
case let .canvasEval(session, javaScript): case let .canvasEval(session, javaScript):
return await self.handleCanvasEval(session: session, javaScript: javaScript) return await self.handleCanvasEval(session: session, javaScript: javaScript)
case let .canvasSnapshot(session, outPath): case let .canvasSnapshot(session, outPath):
return await self.handleCanvasSnapshot(session: session, outPath: outPath) return await self.handleCanvasSnapshot(session: session, outPath: outPath)
case .nodeList: case .nodeList:
return await self.handleNodeList() return await self.handleNodeList()
case let .nodeInvoke(nodeId, command, paramsJSON): case let .nodeInvoke(nodeId, command, paramsJSON):
return await self.handleNodeInvoke( return await self.handleNodeInvoke(
nodeId: nodeId, nodeId: nodeId,
command: command, command: command,
paramsJSON: paramsJSON, paramsJSON: paramsJSON,
logger: logger logger: logger)
) }
} }
}
private struct NotifyRequest { private struct NotifyRequest {
var title: String var title: String
var body: String var body: String
var sound: String? var sound: String?
var priority: NotificationPriority? var priority: NotificationPriority?
var delivery: NotificationDelivery? var delivery: NotificationDelivery?
} }
private static func handleNotify(_ request: NotifyRequest, notifier: NotificationManager) async -> Response { private static func handleNotify(_ request: NotifyRequest, notifier: NotificationManager) async -> Response {
let chosenSound = request.sound?.trimmingCharacters(in: .whitespacesAndNewlines) let chosenSound = request.sound?.trimmingCharacters(in: .whitespacesAndNewlines)
let chosenDelivery = request.delivery ?? .system let chosenDelivery = request.delivery ?? .system
switch chosenDelivery { switch chosenDelivery {
case .system: case .system:
@ -100,8 +98,7 @@ enum ControlRequestHandler {
title: request.title, title: request.title,
body: request.body, body: request.body,
sound: chosenSound, sound: chosenSound,
priority: request.priority priority: request.priority)
)
return ok ? Response(ok: true) : Response(ok: false, message: "notification not authorized") return ok ? Response(ok: true) : Response(ok: false, message: "notification not authorized")
case .overlay: case .overlay:
await MainActor.run { await MainActor.run {
@ -113,8 +110,7 @@ enum ControlRequestHandler {
title: request.title, title: request.title,
body: request.body, body: request.body,
sound: chosenSound, sound: chosenSound,
priority: request.priority priority: request.priority)
)
if ok { return Response(ok: true) } if ok { return Response(ok: true) }
await MainActor.run { await MainActor.run {
NotifyOverlayController.shared.present(title: request.title, body: request.body) NotifyOverlayController.shared.present(title: request.title, body: request.body)
@ -141,8 +137,8 @@ enum ControlRequestHandler {
cwd: String?, cwd: String?,
env: [String: String]?, env: [String: String]?,
timeoutSec: Double?, timeoutSec: Double?,
needsSR: Bool needsSR: Bool) async -> Response
) async -> Response { {
if needsSR { if needsSR {
let authorized = await PermissionManager let authorized = await PermissionManager
.ensure([.screenRecording], interactive: false)[.screenRecording] ?? false .ensure([.screenRecording], interactive: false)[.screenRecording] ?? false
@ -156,8 +152,8 @@ enum ControlRequestHandler {
thinking: String?, thinking: String?,
session: String?, session: String?,
deliver: Bool, deliver: Bool,
to: String? to: String?) async -> Response
) async -> Response { {
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return Response(ok: false, message: "message empty") } guard !trimmed.isEmpty else { return Response(ok: false, message: "message empty") }
let sessionKey = session ?? "main" let sessionKey = session ?? "main"
@ -167,8 +163,7 @@ enum ControlRequestHandler {
sessionKey: sessionKey, sessionKey: sessionKey,
deliver: deliver, deliver: deliver,
to: to, to: to,
channel: nil channel: nil)
)
return rpcResult.ok return rpcResult.ok
? Response(ok: true, message: rpcResult.text ?? "sent") ? Response(ok: true, message: rpcResult.text ?? "sent")
: Response(ok: false, message: rpcResult.error ?? "failed to send") : Response(ok: false, message: rpcResult.error ?? "failed to send")
@ -181,8 +176,8 @@ enum ControlRequestHandler {
private static func handleCanvasShow( private static func handleCanvasShow(
session: String, session: String,
path: String?, path: String?,
placement: CanvasPlacement? placement: CanvasPlacement?) async -> Response
) async -> Response { {
guard self.canvasEnabled() else { return Response(ok: false, message: "Canvas disabled by user") } guard self.canvasEnabled() else { return Response(ok: false, message: "Canvas disabled by user") }
do { do {
let dir = try await MainActor.run { let dir = try await MainActor.run {
@ -235,9 +230,8 @@ enum ControlRequestHandler {
let ids = await BridgeServer.shared.connectedNodeIds() let ids = await BridgeServer.shared.connectedNodeIds()
let payload = (try? JSONSerialization.data( let payload = (try? JSONSerialization.data(
withJSONObject: ["connectedNodeIds": ids], withJSONObject: ["connectedNodeIds": ids],
options: [.prettyPrinted] options: [.prettyPrinted]))
)) .flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
.flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
return Response(ok: true, payload: Data(payload.utf8)) return Response(ok: true, payload: Data(payload.utf8))
} }
@ -245,8 +239,8 @@ enum ControlRequestHandler {
nodeId: String, nodeId: String,
command: String, command: String,
paramsJSON: String?, paramsJSON: String?,
logger: Logger logger: Logger) async -> Response
) async -> Response { {
do { do {
let res = try await BridgeServer.shared.invoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON) let res = try await BridgeServer.shared.invoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON)
if res.ok { if res.ok {

View File

@ -181,42 +181,41 @@ final class WebChatServer: @unchecked Sendable {
status: 403, status: 403,
mime: "text/plain", mime: "text/plain",
body: forbidden, body: forbidden,
contentLength: forbidden.count,
includeBody: includeBody, includeBody: includeBody,
over: connection) over: connection)
return return
} }
guard let data = try? Data(contentsOf: fileURL) else { guard let data = try? Data(contentsOf: fileURL) else {
webChatServerLogger.error("WebChatServer 404 missing \(fileURL.lastPathComponent, privacy: .public)") webChatServerLogger.error("WebChatServer 404 missing \(fileURL.lastPathComponent, privacy: .public)")
self.send( self.send(
status: 404, status: 404,
mime: "text/plain", mime: "text/plain",
body: Data("Not Found".utf8), body: Data("Not Found".utf8),
includeBody: includeBody, includeBody: includeBody,
over: connection) over: connection)
return return
} }
let mime = self.mimeType(forExtension: fileURL.pathExtension) let mime = self.mimeType(forExtension: fileURL.pathExtension)
self.send( self.send(
status: 200, status: 200,
mime: mime, mime: mime,
body: data, body: data,
includeBody: includeBody, includeBody: includeBody,
over: connection) over: connection)
} }
private func send( private func send(
status: Int, status: Int,
mime: String, mime: String,
body: Data, body: Data,
includeBody: Bool, includeBody: Bool,
over connection: NWConnection) over connection: NWConnection)
{ {
let contentLength = body.count let contentLength = body.count
let headers = "HTTP/1.1 \(status) \(statusText(status))\r\n" + let headers = "HTTP/1.1 \(status) \(statusText(status))\r\n" +
"Content-Length: \(contentLength)\r\n" + "Content-Length: \(contentLength)\r\n" +
"Content-Type: \(mime)\r\n" + "Content-Type: \(mime)\r\n" +
"Connection: close\r\n\r\n" "Connection: close\r\n\r\n"
var response = Data(headers.utf8) var response = Data(headers.utf8)
if includeBody { if includeBody {
response.append(body) response.append(body)