chore: format macOS sources
This commit is contained in:
@@ -21,7 +21,7 @@ actor AgentRPC {
|
||||
deliver: Bool,
|
||||
to: String?) async -> (ok: Bool, text: String?, error: String?)
|
||||
{
|
||||
if process?.isRunning != true {
|
||||
if self.process?.isRunning != true {
|
||||
do {
|
||||
try await self.start()
|
||||
} catch {
|
||||
@@ -51,7 +51,8 @@ actor AgentRPC {
|
||||
if let payloadDict = parsed["payload"] as? [String: Any],
|
||||
let payloads = payloadDict["payloads"] as? [[String: Any]],
|
||||
let first = payloads.first,
|
||||
let txt = first["text"] as? String {
|
||||
let txt = first["text"] as? String
|
||||
{
|
||||
return (true, txt, nil)
|
||||
}
|
||||
return (true, nil, nil)
|
||||
@@ -62,14 +63,14 @@ actor AgentRPC {
|
||||
}
|
||||
return (false, nil, "rpc returned unexpected response: \(line)")
|
||||
} catch {
|
||||
logger.error("rpc send failed: \(error.localizedDescription, privacy: .public)")
|
||||
await stop()
|
||||
self.logger.error("rpc send failed: \(error.localizedDescription, privacy: .public)")
|
||||
await self.stop()
|
||||
return (false, nil, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func status() async -> (ok: Bool, error: String?) {
|
||||
if process?.isRunning != true {
|
||||
if self.process?.isRunning != true {
|
||||
do {
|
||||
try await self.start()
|
||||
} catch {
|
||||
@@ -88,14 +89,14 @@ actor AgentRPC {
|
||||
if let ok = parsed?["ok"] as? Bool, ok { return (true, nil) }
|
||||
return (false, parsed?["error"] as? String ?? "rpc status failed: \(line)")
|
||||
} catch {
|
||||
logger.error("rpc status failed: \(error.localizedDescription, privacy: .public)")
|
||||
await stop()
|
||||
self.logger.error("rpc status failed: \(error.localizedDescription, privacy: .public)")
|
||||
await self.stop()
|
||||
return (false, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func setHeartbeatsEnabled(_ enabled: Bool) async -> Bool {
|
||||
guard process?.isRunning == true else { return false }
|
||||
guard self.process?.isRunning == true else { return false }
|
||||
do {
|
||||
let payload: [String: Any] = ["type": "set-heartbeats", "enabled": enabled]
|
||||
let data = try JSONSerialization.data(withJSONObject: payload)
|
||||
@@ -108,8 +109,8 @@ actor AgentRPC {
|
||||
if let ok = parsed?["ok"] as? Bool, ok { return true }
|
||||
return false
|
||||
} catch {
|
||||
logger.error("rpc set-heartbeats failed: \(error.localizedDescription, privacy: .public)")
|
||||
await stop()
|
||||
self.logger.error("rpc set-heartbeats failed: \(error.localizedDescription, privacy: .public)")
|
||||
await self.stop()
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -155,12 +156,12 @@ actor AgentRPC {
|
||||
}
|
||||
|
||||
private func stop() async {
|
||||
stdoutHandle?.readabilityHandler = nil
|
||||
process?.terminate()
|
||||
process = nil
|
||||
stdinHandle = nil
|
||||
stdoutHandle = nil
|
||||
buffer.removeAll(keepingCapacity: false)
|
||||
self.stdoutHandle?.readabilityHandler = nil
|
||||
self.process?.terminate()
|
||||
self.process = nil
|
||||
self.stdinHandle = nil
|
||||
self.stdoutHandle = nil
|
||||
self.buffer.removeAll(keepingCapacity: false)
|
||||
let waiters = self.waiters
|
||||
self.waiters.removeAll()
|
||||
for waiter in waiters {
|
||||
@@ -169,13 +170,13 @@ actor AgentRPC {
|
||||
}
|
||||
|
||||
private func ingest(data: Data) {
|
||||
buffer.append(data)
|
||||
self.buffer.append(data)
|
||||
while let range = buffer.firstRange(of: Data([0x0A])) {
|
||||
let lineData = buffer.subdata(in: buffer.startIndex..<range.lowerBound)
|
||||
buffer.removeSubrange(buffer.startIndex...range.lowerBound)
|
||||
let lineData = self.buffer.subdata(in: self.buffer.startIndex..<range.lowerBound)
|
||||
self.buffer.removeSubrange(self.buffer.startIndex...range.lowerBound)
|
||||
guard let line = String(data: lineData, encoding: .utf8) else { continue }
|
||||
if let waiter = waiters.first {
|
||||
waiters.removeFirst()
|
||||
self.waiters.removeFirst()
|
||||
waiter.resume(returning: line)
|
||||
}
|
||||
}
|
||||
@@ -183,7 +184,7 @@ actor AgentRPC {
|
||||
|
||||
private func nextLine() async throws -> String {
|
||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<String, Error>) in
|
||||
waiters.append(cont)
|
||||
self.waiters.append(cont)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ final class AppState: ObservableObject {
|
||||
UserDefaults.standard.set(true, forKey: heartbeatsEnabledKey)
|
||||
}
|
||||
|
||||
if self.swabbleEnabled && !PermissionManager.voiceWakePermissionsGranted() {
|
||||
if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
|
||||
self.swabbleEnabled = false
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,9 @@ struct DebugSettings: View {
|
||||
Button {
|
||||
Task { await self.reloadModels() }
|
||||
} label: {
|
||||
Label(self.modelsLoading ? "Reloading…" : "Reload models", systemImage: "arrow.clockwise")
|
||||
Label(
|
||||
self.modelsLoading ? "Reloading…" : "Reload models",
|
||||
systemImage: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(self.modelsLoading)
|
||||
@@ -129,7 +131,9 @@ struct DebugSettings: View {
|
||||
}
|
||||
}
|
||||
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)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
@@ -153,7 +157,8 @@ struct DebugSettings: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
} 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)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -172,7 +172,8 @@ struct GeneralSettings: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
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)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -216,8 +217,8 @@ struct GeneralSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
private extension GeneralSettings {
|
||||
func revealLogs() {
|
||||
extension GeneralSettings {
|
||||
private func revealLogs() {
|
||||
let path = URL(fileURLWithPath: "/tmp/clawdis/clawdis.log")
|
||||
if FileManager.default.fileExists(atPath: path.path) {
|
||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import AppKit
|
||||
import Darwin
|
||||
import Foundation
|
||||
import MenuBarExtraAccess
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import Security
|
||||
import OSLog
|
||||
import OSLog
|
||||
import Darwin
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct ClawdisApp: App {
|
||||
@@ -98,11 +97,11 @@ private struct MenuContent: View {
|
||||
|
||||
private func relayLabel(_ status: RelayProcessManager.Status) -> String {
|
||||
switch status {
|
||||
case .running: return "Running"
|
||||
case .starting: return "Starting…"
|
||||
case .restarting: return "Restarting…"
|
||||
case let .failed(reason): return "Failed: \(reason)"
|
||||
case .stopped: return "Stopped"
|
||||
case .running: "Running"
|
||||
case .starting: "Starting…"
|
||||
case .restarting: "Restarting…"
|
||||
case let .failed(reason): "Failed: \(reason)"
|
||||
case .stopped: "Stopped"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +496,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
|
||||
|
||||
// Developer/testing helper: auto-open WebChat when launched with --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")
|
||||
}
|
||||
}
|
||||
@@ -581,7 +580,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
|
||||
var infoCF: CFDictionary?
|
||||
guard SecCodeCopySigningInformation(sCode, SecCSFlags(), &infoCF) == errSecSuccess,
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ enum AppleScriptPermission {
|
||||
let result = appleScript?.executeAndReturnError(&error)
|
||||
|
||||
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)")
|
||||
return false
|
||||
}
|
||||
@@ -180,12 +180,12 @@ enum AppleScriptPermission {
|
||||
/// Triggers the TCC prompt and opens System Settings → Privacy & Security → Automation.
|
||||
@MainActor
|
||||
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.
|
||||
let urlStrings = [
|
||||
"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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Data models
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ enum CLIInstaller {
|
||||
let targetList = targets.map(self.shellEscape).joined(separator: " ")
|
||||
let cmds = [
|
||||
"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: "; ")
|
||||
|
||||
let script = """
|
||||
@@ -180,7 +180,8 @@ enum CommandResolver {
|
||||
|
||||
static func projectRoot() -> URL {
|
||||
if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey),
|
||||
let url = self.expandPath(stored) {
|
||||
let url = self.expandPath(stored)
|
||||
{
|
||||
return url
|
||||
}
|
||||
let fallback = FileManager.default.homeDirectoryForCurrentUser
|
||||
|
||||
@@ -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 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")
|
||||
|
||||
if echoPath {
|
||||
@@ -61,17 +63,16 @@ enum VoiceWakeForwarder {
|
||||
}
|
||||
|
||||
static func commandWithCliPath(_ command: String, target: String, echoCliPath: Bool = false) -> String {
|
||||
let rewritten: String
|
||||
if command.contains("clawdis-mac") {
|
||||
rewritten = command.replacingOccurrences(of: "clawdis-mac", with: "\"$CLI\"")
|
||||
let rewritten: String = if command.contains("clawdis-mac") {
|
||||
command.replacingOccurrences(of: "clawdis-mac", with: "\"$CLI\"")
|
||||
} else {
|
||||
rewritten = "\"$CLI\" \(command)"
|
||||
"\"$CLI\" \(command)"
|
||||
}
|
||||
|
||||
return "\(self.cliLookupPrefix(target: target, echoPath: echoCliPath)); \(rewritten)"
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
// Test-only helpers
|
||||
static func _testSetCliCache(target: String, path: String) {
|
||||
self.cliCache.set((target: target, path: path))
|
||||
@@ -80,7 +81,7 @@ enum VoiceWakeForwarder {
|
||||
static func _testGetCliCache() -> (target: String, path: String)? {
|
||||
self.cliCache.get()
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
enum VoiceWakeForwardError: LocalizedError, Equatable {
|
||||
case invalidTarget
|
||||
@@ -109,7 +110,10 @@ enum VoiceWakeForwarder {
|
||||
}
|
||||
|
||||
@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) }
|
||||
let destination = config.target.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let parsed = self.parse(target: destination) else {
|
||||
@@ -236,7 +240,8 @@ enum VoiceWakeForwarder {
|
||||
if checkProc.terminationStatus == 0 {
|
||||
if let cliLine = statusOut
|
||||
.split(separator: "\n")
|
||||
.last(where: { $0.hasPrefix("__CLI:") }) {
|
||||
.last(where: { $0.hasPrefix("__CLI:") })
|
||||
{
|
||||
let path = String(cliLine.dropFirst("__CLI:".count))
|
||||
if !path.isEmpty {
|
||||
self.cliCache.set((target: destination, path: path))
|
||||
|
||||
@@ -34,32 +34,32 @@ actor VoiceWakeRuntime {
|
||||
}
|
||||
|
||||
guard voiceWakeSupported, snapshot.0 else {
|
||||
stop()
|
||||
self.stop()
|
||||
return
|
||||
}
|
||||
|
||||
guard PermissionManager.voiceWakePermissionsGranted() else {
|
||||
logger.debug("voicewake runtime not starting: permissions missing")
|
||||
stop()
|
||||
self.logger.debug("voicewake runtime not starting: permissions missing")
|
||||
self.stop()
|
||||
return
|
||||
}
|
||||
|
||||
let config = snapshot.1
|
||||
|
||||
if config == currentConfig, recognitionTask != nil {
|
||||
if config == self.currentConfig, self.recognitionTask != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stop()
|
||||
await start(with: config)
|
||||
self.stop()
|
||||
await self.start(with: config)
|
||||
}
|
||||
|
||||
private func start(with config: RuntimeConfig) async {
|
||||
do {
|
||||
configureSession(localeID: config.localeID)
|
||||
self.configureSession(localeID: config.localeID)
|
||||
|
||||
guard let recognizer, recognizer.isAvailable else {
|
||||
logger.error("voicewake runtime: speech recognizer unavailable")
|
||||
self.logger.error("voicewake runtime: speech recognizer unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,19 +67,19 @@ actor VoiceWakeRuntime {
|
||||
self.recognitionRequest?.shouldReportPartialResults = true
|
||||
guard let request = self.recognitionRequest else { return }
|
||||
|
||||
let input = audioEngine.inputNode
|
||||
let input = self.audioEngine.inputNode
|
||||
let format = input.outputFormat(forBus: 0)
|
||||
input.removeTap(onBus: 0)
|
||||
input.installTap(onBus: 0, bufferSize: 2048, format: format) { [weak request] buffer, _ in
|
||||
request?.append(buffer)
|
||||
}
|
||||
|
||||
audioEngine.prepare()
|
||||
try audioEngine.start()
|
||||
self.audioEngine.prepare()
|
||||
try self.audioEngine.start()
|
||||
|
||||
currentConfig = config
|
||||
lastHeard = Date()
|
||||
cooldownUntil = nil
|
||||
self.currentConfig = config
|
||||
self.lastHeard = Date()
|
||||
self.cooldownUntil = nil
|
||||
|
||||
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
|
||||
guard let self else { return }
|
||||
@@ -87,22 +87,22 @@ actor VoiceWakeRuntime {
|
||||
Task { await self.handleRecognition(transcript: transcript, error: error, config: config) }
|
||||
}
|
||||
|
||||
logger.info("voicewake runtime started")
|
||||
self.logger.info("voicewake runtime started")
|
||||
} catch {
|
||||
logger.error("voicewake runtime failed to start: \(error.localizedDescription, privacy: .public)")
|
||||
stop()
|
||||
self.logger.error("voicewake runtime failed to start: \(error.localizedDescription, privacy: .public)")
|
||||
self.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func stop() {
|
||||
recognitionTask?.cancel()
|
||||
recognitionTask = nil
|
||||
recognitionRequest?.endAudio()
|
||||
recognitionRequest = nil
|
||||
audioEngine.inputNode.removeTap(onBus: 0)
|
||||
audioEngine.stop()
|
||||
currentConfig = nil
|
||||
logger.debug("voicewake runtime stopped")
|
||||
self.recognitionTask?.cancel()
|
||||
self.recognitionTask = nil
|
||||
self.recognitionRequest?.endAudio()
|
||||
self.recognitionRequest = nil
|
||||
self.audioEngine.inputNode.removeTap(onBus: 0)
|
||||
self.audioEngine.stop()
|
||||
self.currentConfig = nil
|
||||
self.logger.debug("voicewake runtime stopped")
|
||||
}
|
||||
|
||||
private func configureSession(localeID: String?) {
|
||||
@@ -113,20 +113,21 @@ actor VoiceWakeRuntime {
|
||||
private func handleRecognition(
|
||||
transcript: String?,
|
||||
error: Error?,
|
||||
config: RuntimeConfig) async {
|
||||
config: RuntimeConfig) async
|
||||
{
|
||||
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 }
|
||||
if !transcript.isEmpty { lastHeard = Date() }
|
||||
if !transcript.isEmpty { self.lastHeard = Date() }
|
||||
|
||||
if Self.matches(text: transcript, triggers: config.triggers) {
|
||||
let now = Date()
|
||||
if let cooldown = cooldownUntil, now < cooldown {
|
||||
return
|
||||
}
|
||||
cooldownUntil = now.addingTimeInterval(2.5)
|
||||
self.cooldownUntil = now.addingTimeInterval(2.5)
|
||||
await MainActor.run { AppStateStore.shared.triggerVoiceEars() }
|
||||
let forwardConfig = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
|
||||
if forwardConfig.enabled {
|
||||
@@ -148,9 +149,9 @@ actor VoiceWakeRuntime {
|
||||
return false
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
static func _testMatches(text: String, triggers: [String]) -> Bool {
|
||||
Self.matches(text: text, triggers: triggers)
|
||||
self.matches(text: text, triggers: triggers)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -188,7 +188,8 @@ final class VoiceWakeTester {
|
||||
text: String,
|
||||
isFinal: Bool,
|
||||
errorMessage: String?,
|
||||
onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void) async {
|
||||
onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void) async
|
||||
{
|
||||
if !text.isEmpty {
|
||||
self.lastHeard = Date()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
func start(root: URL) {
|
||||
queue.async {
|
||||
self.queue.async {
|
||||
if self.listener != nil { return }
|
||||
self.root = root
|
||||
let params = NWParameters.tcp
|
||||
@@ -26,8 +26,9 @@ final class WebChatServer: @unchecked Sendable {
|
||||
case .ready:
|
||||
self?.port = listener.port
|
||||
webChatServerLogger.debug("WebChatServer ready on 127.0.0.1:\(listener.port?.rawValue ?? 0)")
|
||||
case .failed(let error):
|
||||
webChatServerLogger.error("WebChatServer failed: \(error.localizedDescription, privacy: .public)")
|
||||
case let .failed(error):
|
||||
webChatServerLogger
|
||||
.error("WebChatServer failed: \(error.localizedDescription, privacy: .public)")
|
||||
self?.listener = nil
|
||||
default:
|
||||
break
|
||||
@@ -39,7 +40,8 @@ final class WebChatServer: @unchecked Sendable {
|
||||
listener.start(queue: self.queue)
|
||||
self.listener = listener
|
||||
} 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.
|
||||
func baseURL() -> URL? {
|
||||
var url: URL?
|
||||
queue.sync {
|
||||
self.queue.sync {
|
||||
if let port {
|
||||
url = URL(string: "http://127.0.0.1:\(port.rawValue)/webchat/")
|
||||
}
|
||||
@@ -60,14 +62,15 @@ final class WebChatServer: @unchecked Sendable {
|
||||
switch state {
|
||||
case .ready:
|
||||
self.receive(on: connection)
|
||||
case .failed(let error):
|
||||
webChatServerLogger.error("WebChatServer connection failed: \(error.localizedDescription, privacy: .public)")
|
||||
case let .failed(error):
|
||||
webChatServerLogger
|
||||
.error("WebChatServer connection failed: \(error.localizedDescription, privacy: .public)")
|
||||
connection.cancel()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
connection.start(queue: queue)
|
||||
connection.start(queue: self.queue)
|
||||
}
|
||||
|
||||
private func receive(on connection: NWConnection) {
|
||||
@@ -109,15 +112,15 @@ final class WebChatServer: @unchecked Sendable {
|
||||
}
|
||||
let fileURL = root.appendingPathComponent(path)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
let mime = mimeType(forExtension: fileURL.pathExtension)
|
||||
send(status: 200, mime: mime, body: data, over: connection)
|
||||
let mime = self.mimeType(forExtension: fileURL.pathExtension)
|
||||
self.send(status: 200, mime: mime, body: data, over: connection)
|
||||
}
|
||||
|
||||
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 {
|
||||
switch code {
|
||||
case 200: return "OK"
|
||||
case 403: return "Forbidden"
|
||||
case 404: return "Not Found"
|
||||
default: return "Error"
|
||||
case 200: "OK"
|
||||
case 403: "Forbidden"
|
||||
case 404: "Not Found"
|
||||
default: "Error"
|
||||
}
|
||||
}
|
||||
|
||||
private func mimeType(forExtension ext: String) -> String {
|
||||
switch ext.lowercased() {
|
||||
case "html", "htm": return "text/html; charset=utf-8"
|
||||
case "js", "mjs": return "application/javascript; charset=utf-8"
|
||||
case "css": return "text/css; charset=utf-8"
|
||||
case "json": return "application/json; charset=utf-8"
|
||||
case "map": return "application/json; charset=utf-8"
|
||||
case "svg": return "image/svg+xml"
|
||||
case "png": return "image/png"
|
||||
case "jpg", "jpeg": return "image/jpeg"
|
||||
case "gif": return "image/gif"
|
||||
case "woff2": return "font/woff2"
|
||||
case "woff": return "font/woff"
|
||||
case "ttf": return "font/ttf"
|
||||
default: return "application/octet-stream"
|
||||
case "html", "htm": "text/html; charset=utf-8"
|
||||
case "js", "mjs": "application/javascript; charset=utf-8"
|
||||
case "css": "text/css; charset=utf-8"
|
||||
case "json": "application/json; charset=utf-8"
|
||||
case "map": "application/json; charset=utf-8"
|
||||
case "svg": "image/svg+xml"
|
||||
case "png": "image/png"
|
||||
case "jpg", "jpeg": "image/jpeg"
|
||||
case "gif": "image/gif"
|
||||
case "woff2": "font/woff2"
|
||||
case "woff": "font/woff"
|
||||
case "ttf": "font/ttf"
|
||||
default: "application/octet-stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,11 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler,
|
||||
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)")
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ struct ClawdisCLI {
|
||||
FileHandle.standardOutput.write(Data([0x0A]))
|
||||
exit(response.ok ? 0 : 1)
|
||||
} catch CLIError.help {
|
||||
printHelp()
|
||||
self.printHelp()
|
||||
exit(0)
|
||||
} catch CLIError.version {
|
||||
printVersion()
|
||||
self.printVersion()
|
||||
exit(0)
|
||||
} catch {
|
||||
fputs("clawdis-mac error: \(error)\n", stderr)
|
||||
@@ -52,6 +52,7 @@ struct ClawdisCLI {
|
||||
switch command {
|
||||
case "--help", "-h", "help":
|
||||
throw CLIError.help
|
||||
|
||||
case "--version", "-V", "version":
|
||||
throw CLIError.version
|
||||
|
||||
@@ -192,7 +193,7 @@ struct ClawdisCLI {
|
||||
}
|
||||
|
||||
private static func printVersion() {
|
||||
let info = loadInfo()
|
||||
let info = self.loadInfo()
|
||||
let version = info["CFBundleShortVersionString"] as? String ?? "unknown"
|
||||
let build = info["CFBundleVersion"] as? String ?? ""
|
||||
let git = info["ClawdisGitCommit"] as? String ?? "unknown"
|
||||
@@ -209,7 +210,8 @@ struct ClawdisCLI {
|
||||
.deletingLastPathComponent() // Contents
|
||||
.appendingPathComponent("Info.plist")
|
||||
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
|
||||
}
|
||||
@@ -217,7 +219,7 @@ struct ClawdisCLI {
|
||||
}
|
||||
|
||||
private static func send(request: Request) async throws -> Response {
|
||||
try await ensureAppRunning()
|
||||
try await self.ensureAppRunning()
|
||||
|
||||
var lastError: Error?
|
||||
for _ in 0..<10 {
|
||||
@@ -246,7 +248,7 @@ struct ClawdisCLI {
|
||||
}
|
||||
|
||||
private static func ensureAppRunning() async throws {
|
||||
let appURL = URL(fileURLWithPath: (CommandLine.arguments.first ?? ""))
|
||||
let appURL = URL(fileURLWithPath: CommandLine.arguments.first ?? "")
|
||||
.resolvingSymlinksInPath()
|
||||
.deletingLastPathComponent() // MacOS
|
||||
.deletingLastPathComponent() // Contents
|
||||
|
||||
Reference in New Issue
Block a user