chore: format macOS sources

This commit is contained in:
Peter Steinberger
2025-12-07 16:35:58 +01:00
parent 45398b7660
commit 040fe58693
14 changed files with 147 additions and 123 deletions

View File

@@ -21,7 +21,7 @@ actor AgentRPC {
deliver: Bool, deliver: Bool,
to: String?) async -> (ok: Bool, text: String?, error: String?) to: String?) async -> (ok: Bool, text: String?, error: String?)
{ {
if process?.isRunning != true { if self.process?.isRunning != true {
do { do {
try await self.start() try await self.start()
} catch { } catch {
@@ -51,7 +51,8 @@ actor AgentRPC {
if let payloadDict = parsed["payload"] as? [String: Any], if let payloadDict = parsed["payload"] as? [String: Any],
let payloads = payloadDict["payloads"] as? [[String: Any]], let payloads = payloadDict["payloads"] as? [[String: Any]],
let first = payloads.first, let first = payloads.first,
let txt = first["text"] as? String { let txt = first["text"] as? String
{
return (true, txt, nil) return (true, txt, nil)
} }
return (true, nil, nil) return (true, nil, nil)
@@ -62,14 +63,14 @@ actor AgentRPC {
} }
return (false, nil, "rpc returned unexpected response: \(line)") return (false, nil, "rpc returned unexpected response: \(line)")
} catch { } catch {
logger.error("rpc send failed: \(error.localizedDescription, privacy: .public)") self.logger.error("rpc send failed: \(error.localizedDescription, privacy: .public)")
await stop() await self.stop()
return (false, nil, error.localizedDescription) return (false, nil, error.localizedDescription)
} }
} }
func status() async -> (ok: Bool, error: String?) { func status() async -> (ok: Bool, error: String?) {
if process?.isRunning != true { if self.process?.isRunning != true {
do { do {
try await self.start() try await self.start()
} catch { } catch {
@@ -88,14 +89,14 @@ actor AgentRPC {
if let ok = parsed?["ok"] as? Bool, ok { return (true, nil) } if let ok = parsed?["ok"] as? Bool, ok { return (true, nil) }
return (false, parsed?["error"] as? String ?? "rpc status failed: \(line)") return (false, parsed?["error"] as? String ?? "rpc status failed: \(line)")
} catch { } catch {
logger.error("rpc status failed: \(error.localizedDescription, privacy: .public)") self.logger.error("rpc status failed: \(error.localizedDescription, privacy: .public)")
await stop() await self.stop()
return (false, error.localizedDescription) return (false, error.localizedDescription)
} }
} }
func setHeartbeatsEnabled(_ enabled: Bool) async -> Bool { func setHeartbeatsEnabled(_ enabled: Bool) async -> Bool {
guard process?.isRunning == true else { return false } guard self.process?.isRunning == true else { return false }
do { do {
let payload: [String: Any] = ["type": "set-heartbeats", "enabled": enabled] let payload: [String: Any] = ["type": "set-heartbeats", "enabled": enabled]
let data = try JSONSerialization.data(withJSONObject: payload) let data = try JSONSerialization.data(withJSONObject: payload)
@@ -108,8 +109,8 @@ actor AgentRPC {
if let ok = parsed?["ok"] as? Bool, ok { return true } if let ok = parsed?["ok"] as? Bool, ok { return true }
return false return false
} catch { } catch {
logger.error("rpc set-heartbeats failed: \(error.localizedDescription, privacy: .public)") self.logger.error("rpc set-heartbeats failed: \(error.localizedDescription, privacy: .public)")
await stop() await self.stop()
return false return false
} }
} }
@@ -155,12 +156,12 @@ actor AgentRPC {
} }
private func stop() async { private func stop() async {
stdoutHandle?.readabilityHandler = nil self.stdoutHandle?.readabilityHandler = nil
process?.terminate() self.process?.terminate()
process = nil self.process = nil
stdinHandle = nil self.stdinHandle = nil
stdoutHandle = nil self.stdoutHandle = nil
buffer.removeAll(keepingCapacity: false) self.buffer.removeAll(keepingCapacity: false)
let waiters = self.waiters let waiters = self.waiters
self.waiters.removeAll() self.waiters.removeAll()
for waiter in waiters { for waiter in waiters {
@@ -169,13 +170,13 @@ actor AgentRPC {
} }
private func ingest(data: Data) { private func ingest(data: Data) {
buffer.append(data) self.buffer.append(data)
while let range = buffer.firstRange(of: Data([0x0A])) { while let range = buffer.firstRange(of: Data([0x0A])) {
let lineData = buffer.subdata(in: buffer.startIndex..<range.lowerBound) let lineData = self.buffer.subdata(in: self.buffer.startIndex..<range.lowerBound)
buffer.removeSubrange(buffer.startIndex...range.lowerBound) self.buffer.removeSubrange(self.buffer.startIndex...range.lowerBound)
guard let line = String(data: lineData, encoding: .utf8) else { continue } guard let line = String(data: lineData, encoding: .utf8) else { continue }
if let waiter = waiters.first { if let waiter = waiters.first {
waiters.removeFirst() self.waiters.removeFirst()
waiter.resume(returning: line) waiter.resume(returning: line)
} }
} }
@@ -183,7 +184,7 @@ actor AgentRPC {
private func nextLine() async throws -> String { private func nextLine() async throws -> String {
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<String, Error>) in try await withCheckedThrowingContinuation { (cont: CheckedContinuation<String, Error>) in
waiters.append(cont) self.waiters.append(cont)
} }
} }
} }

View File

@@ -142,7 +142,7 @@ final class AppState: ObservableObject {
UserDefaults.standard.set(true, forKey: heartbeatsEnabledKey) UserDefaults.standard.set(true, forKey: heartbeatsEnabledKey)
} }
if self.swabbleEnabled && !PermissionManager.voiceWakePermissionsGranted() { if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = false self.swabbleEnabled = false
} }

View File

@@ -109,7 +109,9 @@ struct DebugSettings: View {
Button { Button {
Task { await self.reloadModels() } Task { await self.reloadModels() }
} label: { } label: {
Label(self.modelsLoading ? "Reloading…" : "Reload models", systemImage: "arrow.clockwise") Label(
self.modelsLoading ? "Reloading…" : "Reload models",
systemImage: "arrow.clockwise")
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
.disabled(self.modelsLoading) .disabled(self.modelsLoading)
@@ -129,7 +131,9 @@ struct DebugSettings: View {
} }
} }
Button("Send Test Notification") { Button("Send Test Notification") {
Task { _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) } Task {
_ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil)
}
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
@@ -153,7 +157,8 @@ struct DebugSettings: View {
.font(.caption) .font(.caption)
.foregroundStyle(.red) .foregroundStyle(.red)
} else { } else {
Text("Uses the Voice Wake path: forwards over SSH when configured, otherwise runs locally via rpc.") Text(
"Uses the Voice Wake path: forwards over SSH when configured, otherwise runs locally via rpc.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }

View File

@@ -172,7 +172,8 @@ struct GeneralSettings: View {
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
if let recent = snap.sessions.recent.first { if let recent = snap.sessions.recent.first {
Text("Last activity: \(recent.key) \(recent.updatedAt != nil ? relativeAge(from: Date(timeIntervalSince1970: (recent.updatedAt ?? 0) / 1000)) : "unknown")") Text(
"Last activity: \(recent.key) \(recent.updatedAt != nil ? relativeAge(from: Date(timeIntervalSince1970: (recent.updatedAt ?? 0) / 1000)) : "unknown")")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@@ -216,8 +217,8 @@ struct GeneralSettings: View {
} }
} }
private extension GeneralSettings { extension GeneralSettings {
func revealLogs() { private func revealLogs() {
let path = URL(fileURLWithPath: "/tmp/clawdis/clawdis.log") let path = URL(fileURLWithPath: "/tmp/clawdis/clawdis.log")
if FileManager.default.fileExists(atPath: path.path) { if FileManager.default.fileExists(atPath: path.path) {
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)

View File

@@ -1,11 +1,10 @@
import AppKit import AppKit
import Darwin
import Foundation import Foundation
import MenuBarExtraAccess import MenuBarExtraAccess
import SwiftUI import OSLog
import Security import Security
import OSLog import SwiftUI
import OSLog
import Darwin
@main @main
struct ClawdisApp: App { struct ClawdisApp: App {
@@ -98,11 +97,11 @@ private struct MenuContent: View {
private func relayLabel(_ status: RelayProcessManager.Status) -> String { private func relayLabel(_ status: RelayProcessManager.Status) -> String {
switch status { switch status {
case .running: return "Running" case .running: "Running"
case .starting: return "Starting…" case .starting: "Starting…"
case .restarting: return "Restarting…" case .restarting: "Restarting…"
case let .failed(reason): return "Failed: \(reason)" case let .failed(reason): "Failed: \(reason)"
case .stopped: return "Stopped" case .stopped: "Stopped"
} }
} }
@@ -497,7 +496,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
// Developer/testing helper: auto-open WebChat when launched with --webchat // Developer/testing helper: auto-open WebChat when launched with --webchat
if CommandLine.arguments.contains("--webchat") { if CommandLine.arguments.contains("--webchat") {
webChatAutoLogger.debug("Auto-opening web chat via --webchat flag") self.webChatAutoLogger.debug("Auto-opening web chat via --webchat flag")
WebChatManager.shared.show(sessionKey: "main") WebChatManager.shared.show(sessionKey: "main")
} }
} }
@@ -581,7 +580,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
var infoCF: CFDictionary? var infoCF: CFDictionary?
guard SecCodeCopySigningInformation(sCode, SecCSFlags(), &infoCF) == errSecSuccess, guard SecCodeCopySigningInformation(sCode, SecCSFlags(), &infoCF) == errSecSuccess,
let info = infoCF as? [String: Any], let info = infoCF as? [String: Any],
let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String else { let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String
else {
return false return false
} }

View File

@@ -167,7 +167,7 @@ enum AppleScriptPermission {
let result = appleScript?.executeAndReturnError(&error) let result = appleScript?.executeAndReturnError(&error)
if let error, let code = error["NSAppleScriptErrorNumber"] as? Int { if let error, let code = error["NSAppleScriptErrorNumber"] as? Int {
if code == -1_743 { // errAEEventWouldRequireUserConsent if code == -1743 { // errAEEventWouldRequireUserConsent
Self.logger.debug("AppleScript permission denied (-1743)") Self.logger.debug("AppleScript permission denied (-1743)")
return false return false
} }
@@ -180,12 +180,12 @@ enum AppleScriptPermission {
/// Triggers the TCC prompt and opens System Settings Privacy & Security Automation. /// Triggers the TCC prompt and opens System Settings Privacy & Security Automation.
@MainActor @MainActor
static func requestAuthorization() async { static func requestAuthorization() async {
_ = isAuthorized() // first attempt triggers the dialog if not granted _ = self.isAuthorized() // first attempt triggers the dialog if not granted
// Open the Automation pane to help the user if the prompt was dismissed. // Open the Automation pane to help the user if the prompt was dismissed.
let urlStrings = [ let urlStrings = [
"x-apple.systempreferences:com.apple.preference.security?Privacy_Automation", "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation",
"x-apple.systempreferences:com.apple.preference.security" "x-apple.systempreferences:com.apple.preference.security",
] ]
for candidate in urlStrings { for candidate in urlStrings {

View File

@@ -1,5 +1,5 @@
import SwiftUI
import AppKit import AppKit
import SwiftUI
// MARK: - Data models // MARK: - Data models

View File

@@ -137,7 +137,7 @@ enum CLIInstaller {
let targetList = targets.map(self.shellEscape).joined(separator: " ") let targetList = targets.map(self.shellEscape).joined(separator: " ")
let cmds = [ let cmds = [
"mkdir -p /usr/local/bin /opt/homebrew/bin", "mkdir -p /usr/local/bin /opt/homebrew/bin",
targets.map { "ln -sf \(escapedSource) \($0)" }.joined(separator: "; ") targets.map { "ln -sf \(escapedSource) \($0)" }.joined(separator: "; "),
].joined(separator: "; ") ].joined(separator: "; ")
let script = """ let script = """
@@ -180,7 +180,8 @@ enum CommandResolver {
static func projectRoot() -> URL { static func projectRoot() -> URL {
if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey), if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey),
let url = self.expandPath(stored) { let url = self.expandPath(stored)
{
return url return url
} }
let fallback = FileManager.default.homeDirectoryForCurrentUser let fallback = FileManager.default.homeDirectoryForCurrentUser

View File

@@ -50,7 +50,9 @@ enum VoiceWakeForwarder {
} }
steps.append("if [ -z \"$CLI\" ]; then CLI=$(command -v clawdis-mac 2>/dev/null || true); fi") steps.append("if [ -z \"$CLI\" ]; then CLI=$(command -v clawdis-mac 2>/dev/null || true); fi")
steps.append("if [ -z \"$CLI\" ]; then for c in \(searchList); do [ -x \"$c\" ] && CLI=\"$c\" && break; done; fi") steps
.append(
"if [ -z \"$CLI\" ]; then for c in \(searchList); do [ -x \"$c\" ] && CLI=\"$c\" && break; done; fi")
steps.append("if [ -z \"$CLI\" ]; then echo 'clawdis-mac missing'; exit 127; fi") steps.append("if [ -z \"$CLI\" ]; then echo 'clawdis-mac missing'; exit 127; fi")
if echoPath { if echoPath {
@@ -61,17 +63,16 @@ enum VoiceWakeForwarder {
} }
static func commandWithCliPath(_ command: String, target: String, echoCliPath: Bool = false) -> String { static func commandWithCliPath(_ command: String, target: String, echoCliPath: Bool = false) -> String {
let rewritten: String let rewritten: String = if command.contains("clawdis-mac") {
if command.contains("clawdis-mac") { command.replacingOccurrences(of: "clawdis-mac", with: "\"$CLI\"")
rewritten = command.replacingOccurrences(of: "clawdis-mac", with: "\"$CLI\"")
} else { } else {
rewritten = "\"$CLI\" \(command)" "\"$CLI\" \(command)"
} }
return "\(self.cliLookupPrefix(target: target, echoPath: echoCliPath)); \(rewritten)" return "\(self.cliLookupPrefix(target: target, echoPath: echoCliPath)); \(rewritten)"
} }
#if DEBUG #if DEBUG
// Test-only helpers // Test-only helpers
static func _testSetCliCache(target: String, path: String) { static func _testSetCliCache(target: String, path: String) {
self.cliCache.set((target: target, path: path)) self.cliCache.set((target: target, path: path))
@@ -80,7 +81,7 @@ enum VoiceWakeForwarder {
static func _testGetCliCache() -> (target: String, path: String)? { static func _testGetCliCache() -> (target: String, path: String)? {
self.cliCache.get() self.cliCache.get()
} }
#endif #endif
enum VoiceWakeForwardError: LocalizedError, Equatable { enum VoiceWakeForwardError: LocalizedError, Equatable {
case invalidTarget case invalidTarget
@@ -109,7 +110,10 @@ enum VoiceWakeForwarder {
} }
@discardableResult @discardableResult
static func forward(transcript: String, config: VoiceWakeForwardConfig) async -> Result<Void, VoiceWakeForwardError> { static func forward(
transcript: String,
config: VoiceWakeForwardConfig) async -> Result<Void, VoiceWakeForwardError>
{
guard config.enabled else { return .failure(.disabled) } guard config.enabled else { return .failure(.disabled) }
let destination = config.target.trimmingCharacters(in: .whitespacesAndNewlines) let destination = config.target.trimmingCharacters(in: .whitespacesAndNewlines)
guard let parsed = self.parse(target: destination) else { guard let parsed = self.parse(target: destination) else {
@@ -236,7 +240,8 @@ enum VoiceWakeForwarder {
if checkProc.terminationStatus == 0 { if checkProc.terminationStatus == 0 {
if let cliLine = statusOut if let cliLine = statusOut
.split(separator: "\n") .split(separator: "\n")
.last(where: { $0.hasPrefix("__CLI:") }) { .last(where: { $0.hasPrefix("__CLI:") })
{
let path = String(cliLine.dropFirst("__CLI:".count)) let path = String(cliLine.dropFirst("__CLI:".count))
if !path.isEmpty { if !path.isEmpty {
self.cliCache.set((target: destination, path: path)) self.cliCache.set((target: destination, path: path))

View File

@@ -34,32 +34,32 @@ actor VoiceWakeRuntime {
} }
guard voiceWakeSupported, snapshot.0 else { guard voiceWakeSupported, snapshot.0 else {
stop() self.stop()
return return
} }
guard PermissionManager.voiceWakePermissionsGranted() else { guard PermissionManager.voiceWakePermissionsGranted() else {
logger.debug("voicewake runtime not starting: permissions missing") self.logger.debug("voicewake runtime not starting: permissions missing")
stop() self.stop()
return return
} }
let config = snapshot.1 let config = snapshot.1
if config == currentConfig, recognitionTask != nil { if config == self.currentConfig, self.recognitionTask != nil {
return return
} }
stop() self.stop()
await start(with: config) await self.start(with: config)
} }
private func start(with config: RuntimeConfig) async { private func start(with config: RuntimeConfig) async {
do { do {
configureSession(localeID: config.localeID) self.configureSession(localeID: config.localeID)
guard let recognizer, recognizer.isAvailable else { guard let recognizer, recognizer.isAvailable else {
logger.error("voicewake runtime: speech recognizer unavailable") self.logger.error("voicewake runtime: speech recognizer unavailable")
return return
} }
@@ -67,19 +67,19 @@ actor VoiceWakeRuntime {
self.recognitionRequest?.shouldReportPartialResults = true self.recognitionRequest?.shouldReportPartialResults = true
guard let request = self.recognitionRequest else { return } guard let request = self.recognitionRequest else { return }
let input = audioEngine.inputNode let input = self.audioEngine.inputNode
let format = input.outputFormat(forBus: 0) let format = input.outputFormat(forBus: 0)
input.removeTap(onBus: 0) input.removeTap(onBus: 0)
input.installTap(onBus: 0, bufferSize: 2048, format: format) { [weak request] buffer, _ in input.installTap(onBus: 0, bufferSize: 2048, format: format) { [weak request] buffer, _ in
request?.append(buffer) request?.append(buffer)
} }
audioEngine.prepare() self.audioEngine.prepare()
try audioEngine.start() try self.audioEngine.start()
currentConfig = config self.currentConfig = config
lastHeard = Date() self.lastHeard = Date()
cooldownUntil = nil self.cooldownUntil = nil
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
guard let self else { return } guard let self else { return }
@@ -87,22 +87,22 @@ actor VoiceWakeRuntime {
Task { await self.handleRecognition(transcript: transcript, error: error, config: config) } Task { await self.handleRecognition(transcript: transcript, error: error, config: config) }
} }
logger.info("voicewake runtime started") self.logger.info("voicewake runtime started")
} catch { } catch {
logger.error("voicewake runtime failed to start: \(error.localizedDescription, privacy: .public)") self.logger.error("voicewake runtime failed to start: \(error.localizedDescription, privacy: .public)")
stop() self.stop()
} }
} }
private func stop() { private func stop() {
recognitionTask?.cancel() self.recognitionTask?.cancel()
recognitionTask = nil self.recognitionTask = nil
recognitionRequest?.endAudio() self.recognitionRequest?.endAudio()
recognitionRequest = nil self.recognitionRequest = nil
audioEngine.inputNode.removeTap(onBus: 0) self.audioEngine.inputNode.removeTap(onBus: 0)
audioEngine.stop() self.audioEngine.stop()
currentConfig = nil self.currentConfig = nil
logger.debug("voicewake runtime stopped") self.logger.debug("voicewake runtime stopped")
} }
private func configureSession(localeID: String?) { private func configureSession(localeID: String?) {
@@ -113,20 +113,21 @@ actor VoiceWakeRuntime {
private func handleRecognition( private func handleRecognition(
transcript: String?, transcript: String?,
error: Error?, error: Error?,
config: RuntimeConfig) async { config: RuntimeConfig) async
{
if let error { if let error {
logger.debug("voicewake recognition error: \(error.localizedDescription, privacy: .public)") self.logger.debug("voicewake recognition error: \(error.localizedDescription, privacy: .public)")
} }
guard let transcript else { return } guard let transcript else { return }
if !transcript.isEmpty { lastHeard = Date() } if !transcript.isEmpty { self.lastHeard = Date() }
if Self.matches(text: transcript, triggers: config.triggers) { if Self.matches(text: transcript, triggers: config.triggers) {
let now = Date() let now = Date()
if let cooldown = cooldownUntil, now < cooldown { if let cooldown = cooldownUntil, now < cooldown {
return return
} }
cooldownUntil = now.addingTimeInterval(2.5) self.cooldownUntil = now.addingTimeInterval(2.5)
await MainActor.run { AppStateStore.shared.triggerVoiceEars() } await MainActor.run { AppStateStore.shared.triggerVoiceEars() }
let forwardConfig = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } let forwardConfig = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
if forwardConfig.enabled { if forwardConfig.enabled {
@@ -148,9 +149,9 @@ actor VoiceWakeRuntime {
return false return false
} }
#if DEBUG #if DEBUG
static func _testMatches(text: String, triggers: [String]) -> Bool { static func _testMatches(text: String, triggers: [String]) -> Bool {
Self.matches(text: text, triggers: triggers) self.matches(text: text, triggers: triggers)
} }
#endif #endif
} }

View File

@@ -188,7 +188,8 @@ final class VoiceWakeTester {
text: String, text: String,
isFinal: Bool, isFinal: Bool,
errorMessage: String?, errorMessage: String?,
onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void) async { onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void) async
{
if !text.isEmpty { if !text.isEmpty {
self.lastHeard = Date() self.lastHeard = Date()
} }

View File

@@ -14,7 +14,7 @@ final class WebChatServer: @unchecked Sendable {
/// Start the local HTTP server if it isn't already running. Safe to call multiple times. /// Start the local HTTP server if it isn't already running. Safe to call multiple times.
func start(root: URL) { func start(root: URL) {
queue.async { self.queue.async {
if self.listener != nil { return } if self.listener != nil { return }
self.root = root self.root = root
let params = NWParameters.tcp let params = NWParameters.tcp
@@ -26,8 +26,9 @@ final class WebChatServer: @unchecked Sendable {
case .ready: case .ready:
self?.port = listener.port self?.port = listener.port
webChatServerLogger.debug("WebChatServer ready on 127.0.0.1:\(listener.port?.rawValue ?? 0)") webChatServerLogger.debug("WebChatServer ready on 127.0.0.1:\(listener.port?.rawValue ?? 0)")
case .failed(let error): case let .failed(error):
webChatServerLogger.error("WebChatServer failed: \(error.localizedDescription, privacy: .public)") webChatServerLogger
.error("WebChatServer failed: \(error.localizedDescription, privacy: .public)")
self?.listener = nil self?.listener = nil
default: default:
break break
@@ -39,7 +40,8 @@ final class WebChatServer: @unchecked Sendable {
listener.start(queue: self.queue) listener.start(queue: self.queue)
self.listener = listener self.listener = listener
} catch { } catch {
webChatServerLogger.error("WebChatServer could not start: \(error.localizedDescription, privacy: .public)") webChatServerLogger
.error("WebChatServer could not start: \(error.localizedDescription, privacy: .public)")
} }
} }
} }
@@ -47,7 +49,7 @@ final class WebChatServer: @unchecked Sendable {
/// Returns the base URL once the server is ready, otherwise nil. /// Returns the base URL once the server is ready, otherwise nil.
func baseURL() -> URL? { func baseURL() -> URL? {
var url: URL? var url: URL?
queue.sync { self.queue.sync {
if let port { if let port {
url = URL(string: "http://127.0.0.1:\(port.rawValue)/webchat/") url = URL(string: "http://127.0.0.1:\(port.rawValue)/webchat/")
} }
@@ -60,14 +62,15 @@ final class WebChatServer: @unchecked Sendable {
switch state { switch state {
case .ready: case .ready:
self.receive(on: connection) self.receive(on: connection)
case .failed(let error): case let .failed(error):
webChatServerLogger.error("WebChatServer connection failed: \(error.localizedDescription, privacy: .public)") webChatServerLogger
.error("WebChatServer connection failed: \(error.localizedDescription, privacy: .public)")
connection.cancel() connection.cancel()
default: default:
break break
} }
} }
connection.start(queue: queue) connection.start(queue: self.queue)
} }
private func receive(on connection: NWConnection) { private func receive(on connection: NWConnection) {
@@ -109,15 +112,15 @@ final class WebChatServer: @unchecked Sendable {
} }
let fileURL = root.appendingPathComponent(path) let fileURL = root.appendingPathComponent(path)
guard fileURL.path.hasPrefix(root.path) else { guard fileURL.path.hasPrefix(root.path) else {
send(status: 403, mime: "text/plain", body: Data("Forbidden".utf8), over: connection) self.send(status: 403, mime: "text/plain", body: Data("Forbidden".utf8), over: connection)
return return
} }
guard let data = try? Data(contentsOf: fileURL) else { guard let data = try? Data(contentsOf: fileURL) else {
send(status: 404, mime: "text/plain", body: Data("Not Found".utf8), over: connection) self.send(status: 404, mime: "text/plain", body: Data("Not Found".utf8), over: connection)
return return
} }
let mime = mimeType(forExtension: fileURL.pathExtension) let mime = self.mimeType(forExtension: fileURL.pathExtension)
send(status: 200, mime: mime, body: data, over: connection) self.send(status: 200, mime: mime, body: data, over: connection)
} }
private func send(status: Int, mime: String, body: Data, over connection: NWConnection) { private func send(status: Int, mime: String, body: Data, over connection: NWConnection) {
@@ -134,28 +137,28 @@ final class WebChatServer: @unchecked Sendable {
private func statusText(_ code: Int) -> String { private func statusText(_ code: Int) -> String {
switch code { switch code {
case 200: return "OK" case 200: "OK"
case 403: return "Forbidden" case 403: "Forbidden"
case 404: return "Not Found" case 404: "Not Found"
default: return "Error" default: "Error"
} }
} }
private func mimeType(forExtension ext: String) -> String { private func mimeType(forExtension ext: String) -> String {
switch ext.lowercased() { switch ext.lowercased() {
case "html", "htm": return "text/html; charset=utf-8" case "html", "htm": "text/html; charset=utf-8"
case "js", "mjs": return "application/javascript; charset=utf-8" case "js", "mjs": "application/javascript; charset=utf-8"
case "css": return "text/css; charset=utf-8" case "css": "text/css; charset=utf-8"
case "json": return "application/json; charset=utf-8" case "json": "application/json; charset=utf-8"
case "map": return "application/json; charset=utf-8" case "map": "application/json; charset=utf-8"
case "svg": return "image/svg+xml" case "svg": "image/svg+xml"
case "png": return "image/png" case "png": "image/png"
case "jpg", "jpeg": return "image/jpeg" case "jpg", "jpeg": "image/jpeg"
case "gif": return "image/gif" case "gif": "image/gif"
case "woff2": return "font/woff2" case "woff2": "font/woff2"
case "woff": return "font/woff" case "woff": "font/woff"
case "ttf": return "font/ttf" case "ttf": "font/ttf"
default: return "application/octet-stream" default: "application/octet-stream"
} }
} }
} }

View File

@@ -143,7 +143,11 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler,
webChatLogger.debug("didCommit url=\(webView.url?.absoluteString ?? "nil", privacy: .public)") webChatLogger.debug("didCommit url=\(webView.url?.absoluteString ?? "nil", privacy: .public)")
} }
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: any Error) { func webView(
_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: any Error)
{
webChatLogger.error("didFailProvisional error=\(error.localizedDescription, privacy: .public)") webChatLogger.error("didFailProvisional error=\(error.localizedDescription, privacy: .public)")
} }

View File

@@ -32,10 +32,10 @@ struct ClawdisCLI {
FileHandle.standardOutput.write(Data([0x0A])) FileHandle.standardOutput.write(Data([0x0A]))
exit(response.ok ? 0 : 1) exit(response.ok ? 0 : 1)
} catch CLIError.help { } catch CLIError.help {
printHelp() self.printHelp()
exit(0) exit(0)
} catch CLIError.version { } catch CLIError.version {
printVersion() self.printVersion()
exit(0) exit(0)
} catch { } catch {
fputs("clawdis-mac error: \(error)\n", stderr) fputs("clawdis-mac error: \(error)\n", stderr)
@@ -52,6 +52,7 @@ struct ClawdisCLI {
switch command { switch command {
case "--help", "-h", "help": case "--help", "-h", "help":
throw CLIError.help throw CLIError.help
case "--version", "-V", "version": case "--version", "-V", "version":
throw CLIError.version throw CLIError.version
@@ -192,7 +193,7 @@ struct ClawdisCLI {
} }
private static func printVersion() { private static func printVersion() {
let info = loadInfo() let info = self.loadInfo()
let version = info["CFBundleShortVersionString"] as? String ?? "unknown" let version = info["CFBundleShortVersionString"] as? String ?? "unknown"
let build = info["CFBundleVersion"] as? String ?? "" let build = info["CFBundleVersion"] as? String ?? ""
let git = info["ClawdisGitCommit"] as? String ?? "unknown" let git = info["ClawdisGitCommit"] as? String ?? "unknown"
@@ -209,7 +210,8 @@ struct ClawdisCLI {
.deletingLastPathComponent() // Contents .deletingLastPathComponent() // Contents
.appendingPathComponent("Info.plist") .appendingPathComponent("Info.plist")
if let data = try? Data(contentsOf: url), if let data = try? Data(contentsOf: url),
let dict = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] let dict = try? PropertyListSerialization
.propertyList(from: data, options: [], format: nil) as? [String: Any]
{ {
return dict return dict
} }
@@ -217,7 +219,7 @@ struct ClawdisCLI {
} }
private static func send(request: Request) async throws -> Response { private static func send(request: Request) async throws -> Response {
try await ensureAppRunning() try await self.ensureAppRunning()
var lastError: Error? var lastError: Error?
for _ in 0..<10 { for _ in 0..<10 {
@@ -246,7 +248,7 @@ struct ClawdisCLI {
} }
private static func ensureAppRunning() async throws { private static func ensureAppRunning() async throws {
let appURL = URL(fileURLWithPath: (CommandLine.arguments.first ?? "")) let appURL = URL(fileURLWithPath: CommandLine.arguments.first ?? "")
.resolvingSymlinksInPath() .resolvingSymlinksInPath()
.deletingLastPathComponent() // MacOS .deletingLastPathComponent() // MacOS
.deletingLastPathComponent() // Contents .deletingLastPathComponent() // Contents