chore: run formatters and lint
This commit is contained in:
@@ -1,19 +1,18 @@
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import AsyncXPCConnection
|
||||
import ClawdisIPC
|
||||
import Foundation
|
||||
import class Foundation.Bundle
|
||||
import OSLog
|
||||
import CoreGraphics
|
||||
@preconcurrency import ScreenCaptureKit
|
||||
import AVFoundation
|
||||
import Speech
|
||||
import VideoToolbox
|
||||
import ClawdisIPC
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import MenuBarExtraAccess
|
||||
import OSLog
|
||||
@preconcurrency import ScreenCaptureKit
|
||||
import ServiceManagement
|
||||
import Speech
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import MenuBarExtraAccess
|
||||
import VideoToolbox
|
||||
|
||||
private let serviceName = "com.steipete.clawdis.xpc"
|
||||
private let launchdLabel = "com.steipete.clawdis"
|
||||
@@ -22,6 +21,7 @@ private let currentOnboardingVersion = 2
|
||||
private let pauseDefaultsKey = "clawdis.pauseEnabled"
|
||||
private let swabbleEnabledKey = "clawdis.swabbleEnabled"
|
||||
private let swabbleTriggersKey = "clawdis.swabbleTriggers"
|
||||
private let showDockIconKey = "clawdis.showDockIcon"
|
||||
private let defaultVoiceWakeTriggers = ["clawd", "claude"]
|
||||
private let voiceWakeMicKey = "clawdis.voiceWakeMicID"
|
||||
private let voiceWakeLocaleKey = "clawdis.voiceWakeLocaleID"
|
||||
@@ -35,21 +35,27 @@ final class AppState: ObservableObject {
|
||||
@Published var isPaused: Bool {
|
||||
didSet { UserDefaults.standard.set(isPaused, forKey: pauseDefaultsKey) }
|
||||
}
|
||||
|
||||
@Published var defaultSound: String {
|
||||
didSet { UserDefaults.standard.set(defaultSound, forKey: "clawdis.defaultSound") }
|
||||
}
|
||||
|
||||
@Published var launchAtLogin: Bool {
|
||||
didSet { Task { AppStateStore.updateLaunchAtLogin(enabled: launchAtLogin) } }
|
||||
}
|
||||
|
||||
@Published var onboardingSeen: Bool {
|
||||
didSet { UserDefaults.standard.set(onboardingSeen, forKey: "clawdis.onboardingSeen") }
|
||||
}
|
||||
|
||||
@Published var debugPaneEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
|
||||
}
|
||||
|
||||
@Published var swabbleEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(swabbleEnabled, forKey: swabbleEnabledKey) }
|
||||
}
|
||||
|
||||
@Published var swabbleTriggerWords: [String] {
|
||||
didSet {
|
||||
let cleaned = swabbleTriggerWords.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
@@ -60,12 +66,22 @@ final class AppState: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var showDockIcon: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(showDockIcon, forKey: showDockIconKey)
|
||||
AppActivationPolicy.apply(showDockIcon: showDockIcon)
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeMicID: String {
|
||||
didSet { UserDefaults.standard.set(voiceWakeMicID, forKey: voiceWakeMicKey) }
|
||||
}
|
||||
|
||||
@Published var voiceWakeLocaleID: String {
|
||||
didSet { UserDefaults.standard.set(voiceWakeLocaleID, forKey: voiceWakeLocaleKey) }
|
||||
}
|
||||
|
||||
@Published var voiceWakeAdditionalLocaleIDs: [String] {
|
||||
didSet { UserDefaults.standard.set(voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) }
|
||||
}
|
||||
@@ -79,6 +95,7 @@ final class AppState: ObservableObject {
|
||||
let savedVoiceWake = UserDefaults.standard.bool(forKey: swabbleEnabledKey)
|
||||
self.swabbleEnabled = voiceWakeSupported ? savedVoiceWake : false
|
||||
self.swabbleTriggerWords = UserDefaults.standard.stringArray(forKey: swabbleTriggersKey) ?? defaultVoiceWakeTriggers
|
||||
self.showDockIcon = UserDefaults.standard.bool(forKey: showDockIconKey)
|
||||
self.voiceWakeMicID = UserDefaults.standard.string(forKey: voiceWakeMicKey) ?? ""
|
||||
self.voiceWakeLocaleID = UserDefaults.standard.string(forKey: voiceWakeLocaleKey) ?? Locale.current.identifier
|
||||
self.voiceWakeAdditionalLocaleIDs = UserDefaults.standard.stringArray(forKey: voiceWakeAdditionalLocalesKey) ?? []
|
||||
@@ -100,6 +117,13 @@ enum AppStateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
enum AppActivationPolicy {
|
||||
static func apply(showDockIcon: Bool) {
|
||||
NSApp.setActivationPolicy(showDockIcon ? .regular : .accessory)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - XPC service protocol
|
||||
|
||||
@objc protocol ClawdisXPCProtocol {
|
||||
@@ -135,18 +159,20 @@ final class ClawdisXPCService: NSObject, ClawdisXPCProtocol {
|
||||
|
||||
switch request {
|
||||
case let .notify(title, body, sound):
|
||||
let chosenSound: String
|
||||
if let sound { chosenSound = sound } else { chosenSound = await MainActor.run { AppStateStore.defaultSound } }
|
||||
let chosenSound: String = if let sound { sound } else { await MainActor.run { AppStateStore.defaultSound } }
|
||||
let ok = await notifier.send(title: title, body: body, sound: chosenSound)
|
||||
return ok ? Response(ok: true) : Response(ok: false, message: "notification not authorized")
|
||||
|
||||
case let .ensurePermissions(caps, interactive):
|
||||
let statuses = await PermissionManager.ensure(caps, interactive: interactive)
|
||||
let missing = statuses.filter { !$0.value }.map { $0.key.rawValue }
|
||||
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 Response(ok: true, message: "ready")
|
||||
|
||||
case let .screenshot(displayID, windowID, _):
|
||||
let authorized = await PermissionManager.ensure([.screenRecording], interactive: false)[.screenRecording] ?? false
|
||||
guard authorized else { return Response(ok: false, message: "screen recording permission missing") }
|
||||
@@ -154,6 +180,7 @@ final class ClawdisXPCService: NSObject, ClawdisXPCProtocol {
|
||||
return Response(ok: true, payload: data)
|
||||
}
|
||||
return Response(ok: false, message: "screenshot failed")
|
||||
|
||||
case let .runShell(command, cwd, env, timeoutSec, needsSR):
|
||||
if needsSR {
|
||||
let authorized = await PermissionManager.ensure([.screenRecording], interactive: false)[.screenRecording] ?? false
|
||||
@@ -206,37 +233,41 @@ enum PermissionManager {
|
||||
case .notifications:
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let status = await center.notificationSettings()
|
||||
if status.authorizationStatus == .notDetermined && interactive {
|
||||
if status.authorizationStatus == .notDetermined, interactive {
|
||||
_ = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
let post = await center.notificationSettings()
|
||||
results[cap] = post.authorizationStatus == .authorized
|
||||
} else {
|
||||
results[cap] = status.authorizationStatus == .authorized
|
||||
}
|
||||
|
||||
case .accessibility:
|
||||
// Accessing AX APIs must be on main thread.
|
||||
let trusted = AXIsProcessTrusted()
|
||||
results[cap] = trusted
|
||||
if interactive && !trusted {
|
||||
if interactive, !trusted {
|
||||
_ = AXIsProcessTrustedWithOptions(nil)
|
||||
}
|
||||
|
||||
case .screenRecording:
|
||||
let granted = ScreenRecordingProbe.isAuthorized()
|
||||
if interactive && !granted {
|
||||
if interactive, !granted {
|
||||
await ScreenRecordingProbe.requestAuthorization()
|
||||
}
|
||||
results[cap] = ScreenRecordingProbe.isAuthorized()
|
||||
|
||||
case .microphone:
|
||||
let granted = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
|
||||
if interactive && !granted {
|
||||
if interactive, !granted {
|
||||
let ok = await AVCaptureDevice.requestAccess(for: .audio)
|
||||
results[cap] = ok
|
||||
} else {
|
||||
results[cap] = granted
|
||||
}
|
||||
|
||||
case .speechRecognition:
|
||||
let status = SFSpeechRecognizer.authorizationStatus()
|
||||
if status == .notDetermined && interactive {
|
||||
if status == .notDetermined, interactive {
|
||||
let ok = await withCheckedContinuation { cont in
|
||||
SFSpeechRecognizer.requestAuthorization { auth in cont.resume(returning: auth == .authorized) }
|
||||
}
|
||||
@@ -258,16 +289,20 @@ enum PermissionManager {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let settings = await center.notificationSettings()
|
||||
results[cap] = settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional
|
||||
|
||||
case .accessibility:
|
||||
results[cap] = AXIsProcessTrusted()
|
||||
|
||||
case .screenRecording:
|
||||
if #available(macOS 10.15, *) {
|
||||
results[cap] = CGPreflightScreenCaptureAccess()
|
||||
} else {
|
||||
results[cap] = true
|
||||
}
|
||||
|
||||
case .microphone:
|
||||
results[cap] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
|
||||
|
||||
case .speechRecognition:
|
||||
results[cap] = SFSpeechRecognizer.authorizationStatus() == .authorized
|
||||
}
|
||||
@@ -299,11 +334,10 @@ enum Screenshotter {
|
||||
static func capture(displayID: UInt32?, windowID: UInt32?) async -> Data? {
|
||||
guard let content = try? await SCShareableContent.current else { return nil }
|
||||
|
||||
let targetDisplay: SCDisplay?
|
||||
if let displayID {
|
||||
targetDisplay = content.displays.first(where: { $0.displayID == displayID })
|
||||
let targetDisplay: SCDisplay? = if let displayID {
|
||||
content.displays.first(where: { $0.displayID == displayID })
|
||||
} else {
|
||||
targetDisplay = content.displays.first
|
||||
content.displays.first
|
||||
}
|
||||
|
||||
let filter: SCContentFilter
|
||||
@@ -394,7 +428,7 @@ enum ShellRunner {
|
||||
}
|
||||
|
||||
if let timeout, timeout > 0 {
|
||||
let nanos = UInt64(timeout * 1_000_000_000)
|
||||
let nanos = UInt64(timeout * 1000000000)
|
||||
try? await Task.sleep(nanoseconds: nanos)
|
||||
if process.isRunning {
|
||||
process.terminate()
|
||||
@@ -481,7 +515,7 @@ private struct MenuContent: View {
|
||||
let storePath = SessionLoader.defaultStorePath
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: storePath)),
|
||||
let decoded = try? JSONDecoder().decode([String: SessionEntryRecord].self, from: data) {
|
||||
let sorted = decoded.sorted { (a, b) -> Bool in
|
||||
let sorted = decoded.sorted { a, b -> Bool in
|
||||
let lhs = a.value.updatedAt ?? 0
|
||||
let rhs = b.value.updatedAt ?? 0
|
||||
return lhs > rhs
|
||||
@@ -514,11 +548,9 @@ private struct CritterStatusLabel: View {
|
||||
Image(nsImage: CritterIconRenderer.makeIcon(blink: 0))
|
||||
.frame(width: 18, height: 16)
|
||||
} else {
|
||||
Image(nsImage: CritterIconRenderer.makeIcon(
|
||||
blink: blinkAmount,
|
||||
legWiggle: legWiggle,
|
||||
earWiggle: earWiggle
|
||||
))
|
||||
Image(nsImage: CritterIconRenderer.makeIcon(blink: blinkAmount,
|
||||
legWiggle: legWiggle,
|
||||
earWiggle: earWiggle))
|
||||
.frame(width: 18, height: 16)
|
||||
.rotationEffect(.degrees(wiggleAngle), anchor: .center)
|
||||
.offset(x: wiggleOffset)
|
||||
@@ -641,22 +673,20 @@ enum CritterIconRenderer {
|
||||
// Body
|
||||
ctx.addPath(CGPath(roundedRect: CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH), cornerWidth: bodyCorner, cornerHeight: bodyCorner, transform: nil))
|
||||
// Ears (tiny wiggle)
|
||||
ctx.addPath(CGPath(roundedRect: CGRect(
|
||||
x: bodyX - earW * 0.55 + earWiggle,
|
||||
y: bodyY + bodyH * 0.08 + earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH),
|
||||
cornerWidth: earCorner,
|
||||
cornerHeight: earCorner,
|
||||
transform: nil))
|
||||
ctx.addPath(CGPath(roundedRect: CGRect(
|
||||
x: bodyX + bodyW - earW * 0.45 - earWiggle,
|
||||
y: bodyY + bodyH * 0.08 - earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH),
|
||||
cornerWidth: earCorner,
|
||||
cornerHeight: earCorner,
|
||||
transform: nil))
|
||||
ctx.addPath(CGPath(roundedRect: CGRect(x: bodyX - earW * 0.55 + earWiggle,
|
||||
y: bodyY + bodyH * 0.08 + earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH),
|
||||
cornerWidth: earCorner,
|
||||
cornerHeight: earCorner,
|
||||
transform: nil))
|
||||
ctx.addPath(CGPath(roundedRect: CGRect(x: bodyX + bodyW - earW * 0.45 - earWiggle,
|
||||
y: bodyY + bodyH * 0.08 - earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH),
|
||||
cornerWidth: earCorner,
|
||||
cornerHeight: earCorner,
|
||||
transform: nil))
|
||||
// Legs
|
||||
for i in 0 ..< 4 {
|
||||
let x = legStartX + CGFloat(i) * (legW + legSpacing)
|
||||
@@ -814,19 +844,19 @@ private enum SessionKind {
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .direct: return "Direct"
|
||||
case .group: return "Group"
|
||||
case .global: return "Global"
|
||||
case .unknown: return "Unknown"
|
||||
case .direct: "Direct"
|
||||
case .group: "Group"
|
||||
case .global: "Global"
|
||||
case .unknown: "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var tint: Color {
|
||||
switch self {
|
||||
case .direct: return .accentColor
|
||||
case .group: return .orange
|
||||
case .global: return .purple
|
||||
case .unknown: return .gray
|
||||
case .direct: .accentColor
|
||||
case .group: .orange
|
||||
case .global: .purple
|
||||
case .unknown: .gray
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -849,16 +879,17 @@ private enum SessionLoadError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case let .missingStore(path):
|
||||
return "No session store found at \(path) yet. Send or receive a message to create it."
|
||||
"No session store found at \(path) yet. Send or receive a message to create it."
|
||||
|
||||
case let .decodeFailed(reason):
|
||||
return "Could not read the session store: \(reason)"
|
||||
"Could not read the session store: \(reason)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum SessionLoader {
|
||||
static let fallbackModel = "claude-opus-4-5"
|
||||
static let fallbackContextTokens = 200_000
|
||||
static let fallbackContextTokens = 200000
|
||||
|
||||
static let defaultStorePath = standardize(
|
||||
FileManager.default.homeDirectoryForCurrentUser
|
||||
@@ -868,7 +899,7 @@ private enum SessionLoader {
|
||||
private static let legacyStorePaths: [String] = [
|
||||
standardize(FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".clawdis/sessions.json").path),
|
||||
standardize(FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".warelay/sessions/sessions.json").path),
|
||||
standardize(FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".warelay/sessions.json").path),
|
||||
standardize(FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".warelay/sessions.json").path)
|
||||
]
|
||||
|
||||
static func configHints() -> SessionConfigHints {
|
||||
@@ -890,11 +921,9 @@ private enum SessionLoader {
|
||||
let model = agent?["model"] as? String
|
||||
let contextTokens = (agent?["contextTokens"] as? NSNumber)?.intValue
|
||||
|
||||
return SessionConfigHints(
|
||||
storePath: store.map { standardize($0) },
|
||||
model: model,
|
||||
contextTokens: contextTokens
|
||||
)
|
||||
return SessionConfigHints(storePath: store.map { standardize($0) },
|
||||
model: model,
|
||||
contextTokens: contextTokens)
|
||||
}
|
||||
|
||||
static func resolveStorePath(override: String?) -> String {
|
||||
@@ -928,24 +957,20 @@ private enum SessionLoader {
|
||||
let context = entry.contextTokens ?? defaults.contextTokens
|
||||
let model = entry.model ?? defaults.model
|
||||
|
||||
return SessionRow(
|
||||
id: key,
|
||||
key: key,
|
||||
kind: SessionKind.from(key: key),
|
||||
updatedAt: updated,
|
||||
sessionId: entry.sessionId,
|
||||
thinkingLevel: entry.thinkingLevel,
|
||||
verboseLevel: entry.verboseLevel,
|
||||
systemSent: entry.systemSent ?? false,
|
||||
abortedLastRun: entry.abortedLastRun ?? false,
|
||||
tokens: SessionTokenStats(
|
||||
input: input,
|
||||
output: output,
|
||||
total: total,
|
||||
contextTokens: context
|
||||
),
|
||||
model: model
|
||||
)
|
||||
return SessionRow(id: key,
|
||||
key: key,
|
||||
kind: SessionKind.from(key: key),
|
||||
updatedAt: updated,
|
||||
sessionId: entry.sessionId,
|
||||
thinkingLevel: entry.thinkingLevel,
|
||||
verboseLevel: entry.verboseLevel,
|
||||
systemSent: entry.systemSent ?? false,
|
||||
abortedLastRun: entry.abortedLastRun ?? false,
|
||||
tokens: SessionTokenStats(input: input,
|
||||
output: output,
|
||||
total: total,
|
||||
contextTokens: context),
|
||||
model: model)
|
||||
}
|
||||
.sorted { ($0.updatedAt ?? .distantPast) > ($1.updatedAt ?? .distantPast) }
|
||||
}.value
|
||||
@@ -1056,7 +1081,7 @@ struct SessionsSettings: View {
|
||||
|
||||
private var content: some View {
|
||||
Group {
|
||||
if rows.isEmpty && errorMessage == nil {
|
||||
if rows.isEmpty, errorMessage == nil {
|
||||
Text("No sessions yet. They appear after the first inbound message or heartbeat.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -1117,10 +1142,8 @@ struct SessionsSettings: View {
|
||||
|
||||
let hints = SessionLoader.configHints()
|
||||
let resolvedStore = SessionLoader.resolveStorePath(override: hints.storePath)
|
||||
let defaults = SessionDefaults(
|
||||
model: hints.model ?? SessionLoader.fallbackModel,
|
||||
contextTokens: hints.contextTokens ?? SessionLoader.fallbackContextTokens
|
||||
)
|
||||
let defaults = SessionDefaults(model: hints.model ?? SessionLoader.fallbackModel,
|
||||
contextTokens: hints.contextTokens ?? SessionLoader.fallbackContextTokens)
|
||||
|
||||
do {
|
||||
let newRows = try await SessionLoader.loadRows(at: resolvedStore, defaults: defaults)
|
||||
@@ -1288,7 +1311,7 @@ struct SettingsRootView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: state.debugPaneEnabled) { _, enabled in
|
||||
if !enabled && selectedTab == .debug {
|
||||
if !enabled, selectedTab == .debug {
|
||||
selectedTab = .general
|
||||
}
|
||||
}
|
||||
@@ -1296,7 +1319,7 @@ struct SettingsRootView: View {
|
||||
}
|
||||
|
||||
private func validTab(for requested: SettingsTab) -> SettingsTab {
|
||||
if requested == .debug && !state.debugPaneEnabled { return .general }
|
||||
if requested == .debug, !state.debugPaneEnabled { return .general }
|
||||
return requested
|
||||
}
|
||||
|
||||
@@ -1315,12 +1338,12 @@ enum SettingsTab: CaseIterable {
|
||||
static let windowHeight: CGFloat = 520
|
||||
var title: String {
|
||||
switch self {
|
||||
case .general: return "General"
|
||||
case .sessions: return "Sessions"
|
||||
case .voiceWake: return "Voice Wake"
|
||||
case .permissions: return "Permissions"
|
||||
case .debug: return "Debug"
|
||||
case .about: return "About"
|
||||
case .general: "General"
|
||||
case .sessions: "Sessions"
|
||||
case .voiceWake: "Voice Wake"
|
||||
case .permissions: "Permissions"
|
||||
case .debug: "Debug"
|
||||
case .about: "About"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1399,7 +1422,7 @@ actor MicLevelMonitor {
|
||||
let frameCount = Int(buffer.frameLength)
|
||||
guard frameCount > 0 else { return 0 }
|
||||
var sum: Float = 0
|
||||
for i in 0..<frameCount {
|
||||
for i in 0 ..< frameCount {
|
||||
let s = channel[i]
|
||||
sum += s * s
|
||||
}
|
||||
@@ -1484,13 +1507,11 @@ final class VoiceWakeTester {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func handleResult(
|
||||
matched: Bool,
|
||||
text: String,
|
||||
isFinal: Bool,
|
||||
errorMessage: String?,
|
||||
onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void
|
||||
) {
|
||||
private func handleResult(matched: Bool,
|
||||
text: String,
|
||||
isFinal: Bool,
|
||||
errorMessage: String?,
|
||||
onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void) {
|
||||
if matched, !text.isEmpty {
|
||||
stop()
|
||||
onUpdate(.detected(text))
|
||||
@@ -1520,7 +1541,7 @@ final class VoiceWakeTester {
|
||||
return triggers.contains { lowered.contains($0.lowercased()) }
|
||||
}
|
||||
|
||||
nonisolated private static func ensurePermissions() async throws -> Bool {
|
||||
private nonisolated static func ensurePermissions() async throws -> Bool {
|
||||
let speechStatus = SFSpeechRecognizer.authorizationStatus()
|
||||
if speechStatus == .notDetermined {
|
||||
let granted = await withCheckedContinuation { continuation in
|
||||
@@ -1536,12 +1557,14 @@ final class VoiceWakeTester {
|
||||
let micStatus = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||
switch micStatus {
|
||||
case .authorized: return true
|
||||
|
||||
case .notDetermined:
|
||||
return await withCheckedContinuation { continuation in
|
||||
AVCaptureDevice.requestAccess(for: .audio) { granted in
|
||||
continuation.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -1593,20 +1616,17 @@ struct GeneralSettings: View {
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
SettingsToggleRow(
|
||||
title: "Clawdis active",
|
||||
subtitle: "Pause to stop Clawdis background helpers and notifications.",
|
||||
binding: activeBinding)
|
||||
SettingsToggleRow(title: "Clawdis active",
|
||||
subtitle: "Pause to stop Clawdis background helpers and notifications.",
|
||||
binding: activeBinding)
|
||||
|
||||
SettingsToggleRow(
|
||||
title: "Launch at login",
|
||||
subtitle: "Automatically start Clawdis after you sign in.",
|
||||
binding: $state.launchAtLogin)
|
||||
SettingsToggleRow(title: "Launch at login",
|
||||
subtitle: "Automatically start Clawdis after you sign in.",
|
||||
binding: $state.launchAtLogin)
|
||||
|
||||
SettingsToggleRow(
|
||||
title: "Enable debug tools",
|
||||
subtitle: "Show the Debug tab with development utilities.",
|
||||
binding: $state.debugPaneEnabled)
|
||||
SettingsToggleRow(title: "Enable debug tools",
|
||||
subtitle: "Show the Debug tab with development utilities.",
|
||||
binding: $state.debugPaneEnabled)
|
||||
|
||||
LabeledContent("Default sound") {
|
||||
Picker("Sound", selection: $state.defaultSound) {
|
||||
@@ -1638,10 +1658,8 @@ struct GeneralSettings: View {
|
||||
}
|
||||
|
||||
private var activeBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { !state.isPaused },
|
||||
set: { state.isPaused = !$0 }
|
||||
)
|
||||
Binding(get: { !state.isPaused },
|
||||
set: { state.isPaused = !$0 })
|
||||
}
|
||||
|
||||
private var cliInstaller: some View {
|
||||
@@ -1701,12 +1719,10 @@ struct VoiceWakeSettings: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
SettingsToggleRow(
|
||||
title: "Enable Voice Wake",
|
||||
subtitle: "Listen for a wake phrase (e.g. \"Claude\") before running voice commands.",
|
||||
binding: $state.swabbleEnabled
|
||||
)
|
||||
.disabled(!voiceWakeSupported)
|
||||
SettingsToggleRow(title: "Enable Voice Wake",
|
||||
subtitle: "Listen for a wake phrase (e.g. \"Claude\") before running voice commands.",
|
||||
binding: $state.swabbleEnabled)
|
||||
.disabled(!voiceWakeSupported)
|
||||
|
||||
if !voiceWakeSupported {
|
||||
Label("Voice Wake requires macOS 26 or newer.", systemImage: "exclamationmark.triangle.fill")
|
||||
@@ -1767,16 +1783,16 @@ struct VoiceWakeSettings: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 12)
|
||||
.task { await loadMicsIfNeeded() }
|
||||
.task { await loadLocalesIfNeeded() }
|
||||
.task { await restartMeter() }
|
||||
.onChange(of: state.voiceWakeMicID) { _, _ in
|
||||
Task { await restartMeter() }
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 12)
|
||||
.task { await loadMicsIfNeeded() }
|
||||
.task { await loadLocalesIfNeeded() }
|
||||
.task { await restartMeter() }
|
||||
.onChange(of: state.voiceWakeMicID) { _, _ in
|
||||
Task { await restartMeter() }
|
||||
}
|
||||
.onDisappear {
|
||||
Task { await meter.stop() }
|
||||
}
|
||||
@@ -1824,16 +1840,20 @@ struct VoiceWakeSettings: View {
|
||||
switch testState {
|
||||
case .idle:
|
||||
AnyView(Image(systemName: "waveform").foregroundStyle(.secondary))
|
||||
|
||||
case .requesting:
|
||||
AnyView(ProgressView().controlSize(.small))
|
||||
|
||||
case .listening, .hearing:
|
||||
AnyView(
|
||||
Image(systemName: "ear.and.waveform")
|
||||
.symbolEffect(.pulse)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
)
|
||||
|
||||
case .detected:
|
||||
AnyView(Image(systemName: "checkmark.circle.fill").foregroundStyle(.green))
|
||||
|
||||
case .failed:
|
||||
AnyView(Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.yellow))
|
||||
}
|
||||
@@ -1842,17 +1862,22 @@ struct VoiceWakeSettings: View {
|
||||
private var statusText: String {
|
||||
switch testState {
|
||||
case .idle:
|
||||
return "Press start, say a trigger word, and wait for detection."
|
||||
"Press start, say a trigger word, and wait for detection."
|
||||
|
||||
case .requesting:
|
||||
return "Requesting mic & speech permission…"
|
||||
"Requesting mic & speech permission…"
|
||||
|
||||
case .listening:
|
||||
return "Listening… say your trigger word."
|
||||
"Listening… say your trigger word."
|
||||
|
||||
case let .hearing(text):
|
||||
return "Heard: \(text)"
|
||||
"Heard: \(text)"
|
||||
|
||||
case .detected:
|
||||
return "Voice wake detected!"
|
||||
"Voice wake detected!"
|
||||
|
||||
case let .failed(reason):
|
||||
return reason
|
||||
reason
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1866,16 +1891,14 @@ struct VoiceWakeSettings: View {
|
||||
}
|
||||
|
||||
private func binding(for index: Int) -> Binding<String> {
|
||||
Binding(
|
||||
get: {
|
||||
guard state.swabbleTriggerWords.indices.contains(index) else { return "" }
|
||||
return state.swabbleTriggerWords[index]
|
||||
},
|
||||
set: { newValue in
|
||||
guard state.swabbleTriggerWords.indices.contains(index) else { return }
|
||||
state.swabbleTriggerWords[index] = newValue
|
||||
}
|
||||
)
|
||||
Binding(get: {
|
||||
guard state.swabbleTriggerWords.indices.contains(index) else { return "" }
|
||||
return state.swabbleTriggerWords[index]
|
||||
},
|
||||
set: { newValue in
|
||||
guard state.swabbleTriggerWords.indices.contains(index) else { return }
|
||||
state.swabbleTriggerWords[index] = newValue
|
||||
})
|
||||
}
|
||||
|
||||
private func toggleTest() {
|
||||
@@ -1895,20 +1918,18 @@ struct VoiceWakeSettings: View {
|
||||
testState = .requesting
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await tester.start(
|
||||
triggers: triggers,
|
||||
micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID,
|
||||
localeID: state.voiceWakeLocaleID,
|
||||
onUpdate: { newState in
|
||||
DispatchQueue.main.async { [self] in
|
||||
testState = newState
|
||||
if case .detected = newState { isTesting = false }
|
||||
if case .failed = newState { isTesting = false }
|
||||
}
|
||||
}
|
||||
)
|
||||
try await tester.start(triggers: triggers,
|
||||
micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID,
|
||||
localeID: state.voiceWakeLocaleID,
|
||||
onUpdate: { newState in
|
||||
DispatchQueue.main.async { [self] in
|
||||
testState = newState
|
||||
if case .detected = newState { isTesting = false }
|
||||
if case .failed = newState { isTesting = false }
|
||||
}
|
||||
})
|
||||
// timeout after 10s
|
||||
try await Task.sleep(nanoseconds: 10 * 1_000_000_000)
|
||||
try await Task.sleep(nanoseconds: 10 * 1000000000)
|
||||
if isTesting {
|
||||
tester.stop()
|
||||
testState = .failed("Timeout: no trigger heard")
|
||||
@@ -1953,7 +1974,7 @@ struct VoiceWakeSettings: View {
|
||||
Picker("Language", selection: $state.voiceWakeLocaleID) {
|
||||
let current = Locale(identifier: Locale.current.identifier)
|
||||
Text("\(friendlyName(for: current)) (System)").tag(Locale.current.identifier)
|
||||
ForEach(availableLocales.map { $0.identifier }, id: \.self) { id in
|
||||
ForEach(availableLocales.map(\.identifier), id: \.self) { id in
|
||||
if id != Locale.current.identifier {
|
||||
Text(friendlyName(for: Locale(identifier: id))).tag(id)
|
||||
}
|
||||
@@ -1969,14 +1990,12 @@ struct VoiceWakeSettings: View {
|
||||
.font(.footnote.weight(.semibold))
|
||||
ForEach(Array(state.voiceWakeAdditionalLocaleIDs.enumerated()), id: \.offset) { idx, localeID in
|
||||
HStack(spacing: 8) {
|
||||
Picker("Extra \(idx + 1)", selection: Binding(
|
||||
get: { localeID },
|
||||
set: { newValue in
|
||||
guard state.voiceWakeAdditionalLocaleIDs.indices.contains(idx) else { return }
|
||||
state.voiceWakeAdditionalLocaleIDs[idx] = newValue
|
||||
}
|
||||
)) {
|
||||
ForEach(availableLocales.map { $0.identifier }, id: \.self) { id in
|
||||
Picker("Extra \(idx + 1)", selection: Binding(get: { localeID },
|
||||
set: { newValue in
|
||||
guard state.voiceWakeAdditionalLocaleIDs.indices.contains(idx) else { return }
|
||||
state.voiceWakeAdditionalLocaleIDs[idx] = newValue
|
||||
})) {
|
||||
ForEach(availableLocales.map(\.identifier), id: \.self) { id in
|
||||
Text(friendlyName(for: Locale(identifier: id))).tag(id)
|
||||
}
|
||||
}
|
||||
@@ -2027,11 +2046,9 @@ struct VoiceWakeSettings: View {
|
||||
private func loadMicsIfNeeded() async {
|
||||
guard availableMics.isEmpty, !loadingMics else { return }
|
||||
loadingMics = true
|
||||
let discovery = AVCaptureDevice.DiscoverySession(
|
||||
deviceTypes: [.external, .microphone],
|
||||
mediaType: .audio,
|
||||
position: .unspecified
|
||||
)
|
||||
let discovery = AVCaptureDevice.DiscoverySession(deviceTypes: [.external, .microphone],
|
||||
mediaType: .audio,
|
||||
position: .unspecified)
|
||||
availableMics = discovery.devices.map { AudioInputDevice(uid: $0.uniqueID, name: $0.localizedName) }
|
||||
loadingMics = false
|
||||
}
|
||||
@@ -2305,12 +2322,16 @@ struct PermissionStatusList: View {
|
||||
case .notifications:
|
||||
let center = UNUserNotificationCenter.current()
|
||||
_ = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
|
||||
case .accessibility:
|
||||
openSettings("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")
|
||||
|
||||
case .screenRecording:
|
||||
openSettings("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording")
|
||||
|
||||
case .microphone:
|
||||
openSettings("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")
|
||||
|
||||
case .speechRecognition:
|
||||
openSettings("x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition")
|
||||
}
|
||||
@@ -2400,31 +2421,31 @@ private struct PermissionRow: View {
|
||||
|
||||
private var title: String {
|
||||
switch capability {
|
||||
case .notifications: return "Notifications"
|
||||
case .accessibility: return "Accessibility"
|
||||
case .screenRecording: return "Screen Recording"
|
||||
case .microphone: return "Microphone"
|
||||
case .speechRecognition: return "Speech Recognition"
|
||||
case .notifications: "Notifications"
|
||||
case .accessibility: "Accessibility"
|
||||
case .screenRecording: "Screen Recording"
|
||||
case .microphone: "Microphone"
|
||||
case .speechRecognition: "Speech Recognition"
|
||||
}
|
||||
}
|
||||
|
||||
private var subtitle: String {
|
||||
switch capability {
|
||||
case .notifications: return "Show desktop alerts for agent activity"
|
||||
case .accessibility: return "Control UI elements when an action requires it"
|
||||
case .screenRecording: return "Capture the screen for context or screenshots"
|
||||
case .microphone: return "Allow Voice Wake and audio capture"
|
||||
case .speechRecognition: return "Transcribe Voice Wake trigger phrases on-device"
|
||||
case .notifications: "Show desktop alerts for agent activity"
|
||||
case .accessibility: "Control UI elements when an action requires it"
|
||||
case .screenRecording: "Capture the screen for context or screenshots"
|
||||
case .microphone: "Allow Voice Wake and audio capture"
|
||||
case .speechRecognition: "Transcribe Voice Wake trigger phrases on-device"
|
||||
}
|
||||
}
|
||||
|
||||
private var icon: String {
|
||||
switch capability {
|
||||
case .notifications: return "bell"
|
||||
case .accessibility: return "hand.raised"
|
||||
case .screenRecording: return "display"
|
||||
case .microphone: return "mic"
|
||||
case .speechRecognition: return "waveform"
|
||||
case .notifications: "bell"
|
||||
case .accessibility: "hand.raised"
|
||||
case .screenRecording: "display"
|
||||
case .microphone: "mic"
|
||||
case .speechRecognition: "waveform"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2435,7 +2456,7 @@ struct MicLevelBar: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 3) {
|
||||
ForEach(0..<segments, id: \.self) { idx in
|
||||
ForEach(0 ..< segments, id: \.self) { idx in
|
||||
let fill = level * Double(segments) > Double(idx)
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(fill ? segmentColor(for: idx) : Color.gray.opacity(0.35))
|
||||
@@ -2519,10 +2540,8 @@ struct OnboardingView: View {
|
||||
readyPage().frame(width: pageWidth)
|
||||
}
|
||||
.offset(x: CGFloat(-currentPage) * pageWidth)
|
||||
.animation(
|
||||
.interactiveSpring(response: 0.5, dampingFraction: 0.86, blendDuration: 0.25),
|
||||
value: currentPage
|
||||
)
|
||||
.animation(.interactiveSpring(response: 0.5, dampingFraction: 0.86, blendDuration: 0.25),
|
||||
value: currentPage)
|
||||
.frame(width: pageWidth, height: contentHeight, alignment: .top)
|
||||
.clipped()
|
||||
}
|
||||
@@ -2560,21 +2579,15 @@ struct OnboardingView: View {
|
||||
Text("What Clawdis handles")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
onboardingCard {
|
||||
featureRow(
|
||||
title: "Owns the TCC prompts",
|
||||
subtitle: "Requests Notifications, Accessibility, and Screen Recording so your agents stay unblocked.",
|
||||
systemImage: "lock.shield"
|
||||
)
|
||||
featureRow(
|
||||
title: "Native notifications",
|
||||
subtitle: "Shows desktop toasts for agent events with your preferred sound.",
|
||||
systemImage: "bell.and.waveform"
|
||||
)
|
||||
featureRow(
|
||||
title: "Privileged helpers",
|
||||
subtitle: "Runs screenshots or shell actions from the `clawdis-mac` CLI with the right permissions.",
|
||||
systemImage: "terminal"
|
||||
)
|
||||
featureRow(title: "Owns the TCC prompts",
|
||||
subtitle: "Requests Notifications, Accessibility, and Screen Recording so your agents stay unblocked.",
|
||||
systemImage: "lock.shield")
|
||||
featureRow(title: "Native notifications",
|
||||
subtitle: "Shows desktop toasts for agent events with your preferred sound.",
|
||||
systemImage: "bell.and.waveform")
|
||||
featureRow(title: "Privileged helpers",
|
||||
subtitle: "Runs screenshots or shell actions from the `clawdis-mac` CLI with the right permissions.",
|
||||
systemImage: "terminal")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2683,16 +2696,12 @@ struct OnboardingView: View {
|
||||
Text("All set")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
onboardingCard {
|
||||
featureRow(
|
||||
title: "Run the dashboard",
|
||||
subtitle: "Use the CLI helper from your scripts, and reopen onboarding from Settings if you add a new user.",
|
||||
systemImage: "checkmark.seal"
|
||||
)
|
||||
featureRow(
|
||||
title: "Test a notification",
|
||||
subtitle: "Send a quick notify via the menu bar to confirm sounds and permissions.",
|
||||
systemImage: "bell.badge"
|
||||
)
|
||||
featureRow(title: "Run the dashboard",
|
||||
subtitle: "Use the CLI helper from your scripts, and reopen onboarding from Settings if you add a new user.",
|
||||
systemImage: "checkmark.seal")
|
||||
featureRow(title: "Test a notification",
|
||||
subtitle: "Send a quick notify via the menu bar to confirm sounds and permissions.",
|
||||
systemImage: "bell.badge")
|
||||
}
|
||||
Text("Finish to save this version of onboarding. We'll reshow automatically when steps change.")
|
||||
.font(.footnote)
|
||||
@@ -2728,7 +2737,7 @@ struct OnboardingView: View {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<pageCount, id: \.self) { index in
|
||||
ForEach(0 ..< pageCount, id: \.self) { index in
|
||||
Button {
|
||||
withAnimation { currentPage = index }
|
||||
} label: {
|
||||
@@ -2858,14 +2867,12 @@ private struct GlowingClawdisIcon: View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.accentColor.opacity(glowIntensity),
|
||||
Color.blue.opacity(glowIntensity * 0.6)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
LinearGradient(colors: [
|
||||
Color.accentColor.opacity(glowIntensity),
|
||||
Color.blue.opacity(glowIntensity * 0.6)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing)
|
||||
)
|
||||
.blur(radius: 22)
|
||||
.scaleEffect(breathe ? 1.12 : 0.95)
|
||||
|
||||
@@ -14,16 +14,15 @@ struct ClawdisCLI {
|
||||
do {
|
||||
let request = try parseCommandLine()
|
||||
let response = try await send(request: request)
|
||||
let payloadString: String?
|
||||
if let payload = response.payload, let text = String(data: payload, encoding: .utf8) {
|
||||
payloadString = text
|
||||
let payloadString: String? = if let payload = response.payload, let text = String(data: payload, encoding: .utf8) {
|
||||
text
|
||||
} else {
|
||||
payloadString = nil
|
||||
nil
|
||||
}
|
||||
let output: [String: Any] = [
|
||||
"ok": response.ok,
|
||||
"message": response.message ?? "",
|
||||
"payload": payloadString ?? "",
|
||||
"payload": payloadString ?? ""
|
||||
]
|
||||
let json = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted])
|
||||
FileHandle.standardOutput.write(json)
|
||||
@@ -56,6 +55,7 @@ struct ClawdisCLI {
|
||||
}
|
||||
guard let t = title, let b = body else { throw CLIError.help }
|
||||
return .notify(title: t, body: b, sound: sound)
|
||||
|
||||
case "ensure-permissions":
|
||||
var caps: [Capability] = []
|
||||
var interactive = false
|
||||
@@ -70,6 +70,7 @@ struct ClawdisCLI {
|
||||
}
|
||||
if caps.isEmpty { caps = Capability.allCases }
|
||||
return .ensurePermissions(caps, interactive: interactive)
|
||||
|
||||
case "screenshot":
|
||||
var displayID: UInt32?
|
||||
var windowID: UInt32?
|
||||
@@ -82,6 +83,7 @@ struct ClawdisCLI {
|
||||
}
|
||||
}
|
||||
return .screenshot(displayID: displayID, windowID: windowID, format: "png")
|
||||
|
||||
case "run":
|
||||
var cwd: String?
|
||||
var env: [String: String] = [:]
|
||||
@@ -92,19 +94,23 @@ struct ClawdisCLI {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--cwd": cwd = args.popFirst()
|
||||
|
||||
case "--env":
|
||||
if let pair = args.popFirst(), let eq = pair.firstIndex(of: "=") {
|
||||
let k = String(pair[..<eq]); let v = String(pair[pair.index(after: eq)...]); env[k] = v
|
||||
}
|
||||
case "--timeout": if let val = args.popFirst(), let dbl = Double(val) { timeout = dbl }
|
||||
case "--needs-screen-recording": needsSR = true
|
||||
|
||||
default:
|
||||
cmd.append(arg)
|
||||
}
|
||||
}
|
||||
return .runShell(command: cmd, cwd: cwd, env: env.isEmpty ? nil : env, timeoutSec: timeout, needsScreenRecording: needsSR)
|
||||
|
||||
case "status":
|
||||
return .status
|
||||
|
||||
default:
|
||||
throw CLIError.help
|
||||
}
|
||||
@@ -131,7 +137,7 @@ struct ClawdisCLI {
|
||||
|
||||
enum CLIError: Error { case help }
|
||||
|
||||
extension Array where Element == String {
|
||||
extension [String] {
|
||||
mutating func popFirst() -> String? {
|
||||
guard let first else { return nil }
|
||||
self = Array(self.dropFirst())
|
||||
|
||||
@@ -62,15 +62,18 @@ extension Request: Codable {
|
||||
try container.encode(title, forKey: .title)
|
||||
try container.encode(body, forKey: .body)
|
||||
try container.encodeIfPresent(sound, forKey: .sound)
|
||||
|
||||
case let .ensurePermissions(caps, interactive):
|
||||
try container.encode(Kind.ensurePermissions, forKey: .type)
|
||||
try container.encode(caps, forKey: .caps)
|
||||
try container.encode(interactive, forKey: .interactive)
|
||||
|
||||
case let .screenshot(displayID, windowID, format):
|
||||
try container.encode(Kind.screenshot, forKey: .type)
|
||||
try container.encodeIfPresent(displayID, forKey: .displayID)
|
||||
try container.encodeIfPresent(windowID, forKey: .windowID)
|
||||
try container.encode(format, forKey: .format)
|
||||
|
||||
case let .runShell(command, cwd, env, timeoutSec, needsSR):
|
||||
try container.encode(Kind.runShell, forKey: .type)
|
||||
try container.encode(command, forKey: .command)
|
||||
@@ -78,6 +81,7 @@ extension Request: Codable {
|
||||
try container.encodeIfPresent(env, forKey: .env)
|
||||
try container.encodeIfPresent(timeoutSec, forKey: .timeoutSec)
|
||||
try container.encode(needsSR, forKey: .needsScreenRecording)
|
||||
|
||||
case .status:
|
||||
try container.encode(Kind.status, forKey: .type)
|
||||
}
|
||||
@@ -92,15 +96,18 @@ extension Request: Codable {
|
||||
let body = try container.decode(String.self, forKey: .body)
|
||||
let sound = try container.decodeIfPresent(String.self, forKey: .sound)
|
||||
self = .notify(title: title, body: body, sound: sound)
|
||||
|
||||
case .ensurePermissions:
|
||||
let caps = try container.decode([Capability].self, forKey: .caps)
|
||||
let interactive = try container.decode(Bool.self, forKey: .interactive)
|
||||
self = .ensurePermissions(caps, interactive: interactive)
|
||||
|
||||
case .screenshot:
|
||||
let displayID = try container.decodeIfPresent(UInt32.self, forKey: .displayID)
|
||||
let windowID = try container.decodeIfPresent(UInt32.self, forKey: .windowID)
|
||||
let format = try container.decode(String.self, forKey: .format)
|
||||
self = .screenshot(displayID: displayID, windowID: windowID, format: format)
|
||||
|
||||
case .runShell:
|
||||
let command = try container.decode([String].self, forKey: .command)
|
||||
let cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
|
||||
@@ -108,6 +115,7 @@ extension Request: Codable {
|
||||
let timeout = try container.decodeIfPresent(Double.self, forKey: .timeoutSec)
|
||||
let needsSR = try container.decode(Bool.self, forKey: .needsScreenRecording)
|
||||
self = .runShell(command: command, cwd: cwd, env: env, timeoutSec: timeout, needsScreenRecording: needsSR)
|
||||
|
||||
case .status:
|
||||
self = .status
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user