chore(swift): run swiftformat and clear swiftlint
This commit is contained in:
@@ -51,8 +51,7 @@ actor BridgeClient {
|
||||
nodeId: hello.nodeId,
|
||||
displayName: hello.displayName,
|
||||
platform: hello.platform,
|
||||
version: hello.version
|
||||
),
|
||||
version: hello.version),
|
||||
over: connection)
|
||||
|
||||
onStatus?("Waiting for approval…")
|
||||
|
||||
@@ -42,36 +42,36 @@ final class NodeAppModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func setVoiceWakeEnabled(_ enabled: Bool) {
|
||||
self.voiceWake.setEnabled(enabled)
|
||||
}
|
||||
func setVoiceWakeEnabled(_ enabled: Bool) {
|
||||
self.voiceWake.setEnabled(enabled)
|
||||
}
|
||||
|
||||
func connectToBridge(
|
||||
endpoint: NWEndpoint,
|
||||
hello: BridgeHello)
|
||||
{
|
||||
self.bridgeTask?.cancel()
|
||||
self.bridgeStatusText = "Connecting…"
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
self.connectedBridgeID = BridgeEndpointID.stableID(endpoint)
|
||||
func connectToBridge(
|
||||
endpoint: NWEndpoint,
|
||||
hello: BridgeHello)
|
||||
{
|
||||
self.bridgeTask?.cancel()
|
||||
self.bridgeStatusText = "Connecting…"
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
self.connectedBridgeID = BridgeEndpointID.stableID(endpoint)
|
||||
|
||||
self.bridgeTask = Task {
|
||||
do {
|
||||
try await self.bridge.connect(
|
||||
endpoint: endpoint,
|
||||
hello: hello,
|
||||
onConnected: { [weak self] serverName in
|
||||
guard let self else { return }
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Connected"
|
||||
do {
|
||||
try await self.bridge.connect(
|
||||
endpoint: endpoint,
|
||||
hello: hello,
|
||||
onConnected: { [weak self] serverName in
|
||||
guard let self else { return }
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Connected"
|
||||
self.bridgeServerName = serverName
|
||||
}
|
||||
if let addr = await self.bridge.currentRemoteAddress() {
|
||||
await MainActor.run {
|
||||
self.bridgeRemoteAddress = addr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onInvoke: { [weak self] req in
|
||||
guard let self else {
|
||||
@@ -110,20 +110,20 @@ final class NodeAppModel: ObservableObject {
|
||||
self.connectedBridgeID = nil
|
||||
}
|
||||
|
||||
func sendVoiceTranscript(text: String, sessionKey: String?) async throws {
|
||||
struct Payload: Codable {
|
||||
var text: String
|
||||
var sessionKey: String?
|
||||
}
|
||||
let payload = Payload(text: text, sessionKey: sessionKey)
|
||||
let data = try JSONEncoder().encode(payload)
|
||||
guard let json = String(bytes: data, encoding: .utf8) else {
|
||||
throw NSError(domain: "NodeAppModel", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to encode voice transcript payload as UTF-8",
|
||||
])
|
||||
}
|
||||
try await self.bridge.sendEvent(event: "voice.transcript", payloadJSON: json)
|
||||
}
|
||||
func sendVoiceTranscript(text: String, sessionKey: String?) async throws {
|
||||
struct Payload: Codable {
|
||||
var text: String
|
||||
var sessionKey: String?
|
||||
}
|
||||
let payload = Payload(text: text, sessionKey: sessionKey)
|
||||
let data = try JSONEncoder().encode(payload)
|
||||
guard let json = String(bytes: data, encoding: .utf8) else {
|
||||
throw NSError(domain: "NodeAppModel", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to encode voice transcript payload as UTF-8",
|
||||
])
|
||||
}
|
||||
try await self.bridge.sendEvent(event: "voice.transcript", payloadJSON: json)
|
||||
}
|
||||
|
||||
func handleDeepLink(url: URL) async {
|
||||
guard let route = DeepLinkParser.parse(url) else { return }
|
||||
@@ -163,16 +163,16 @@ final class NodeAppModel: ObservableObject {
|
||||
])
|
||||
}
|
||||
|
||||
// iOS bridge forwards to the gateway; no local auth prompts here.
|
||||
// (Key-based unattended auth is handled on macOS for clawdis:// links.)
|
||||
let data = try JSONEncoder().encode(link)
|
||||
guard let json = String(bytes: data, encoding: .utf8) else {
|
||||
throw NSError(domain: "NodeAppModel", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to encode agent request payload as UTF-8",
|
||||
])
|
||||
}
|
||||
try await self.bridge.sendEvent(event: "agent.request", payloadJSON: json)
|
||||
}
|
||||
// iOS bridge forwards to the gateway; no local auth prompts here.
|
||||
// (Key-based unattended auth is handled on macOS for clawdis:// links.)
|
||||
let data = try JSONEncoder().encode(link)
|
||||
guard let json = String(bytes: data, encoding: .utf8) else {
|
||||
throw NSError(domain: "NodeAppModel", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to encode agent request payload as UTF-8",
|
||||
])
|
||||
}
|
||||
try await self.bridge.sendEvent(event: "agent.request", payloadJSON: json)
|
||||
}
|
||||
|
||||
private func isBridgeConnected() async -> Bool {
|
||||
if case .connected = await self.bridge.state { return true }
|
||||
@@ -243,13 +243,13 @@ final class NodeAppModel: ObservableObject {
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
}
|
||||
|
||||
private static func encodePayload(_ obj: some Encodable) throws -> String {
|
||||
let data = try JSONEncoder().encode(obj)
|
||||
guard let json = String(bytes: data, encoding: .utf8) else {
|
||||
throw NSError(domain: "NodeAppModel", code: 21, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to encode payload as UTF-8",
|
||||
])
|
||||
}
|
||||
return json
|
||||
}
|
||||
private static func encodePayload(_ obj: some Encodable) throws -> String {
|
||||
let data = try JSONEncoder().encode(obj)
|
||||
guard let json = String(bytes: data, encoding: .utf8) else {
|
||||
throw NSError(domain: "NodeAppModel", code: 21, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to encode payload as UTF-8",
|
||||
])
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,19 +105,19 @@ final class ScreenController: ObservableObject {
|
||||
#000;
|
||||
overflow: hidden;
|
||||
}
|
||||
body::before {
|
||||
content:"";
|
||||
position: fixed;
|
||||
inset: -20%;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px,
|
||||
transparent 1px, transparent 48px),
|
||||
repeating-linear-gradient(90deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px,
|
||||
transparent 1px, transparent 48px);
|
||||
transform: rotate(-7deg);
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
body::before {
|
||||
content:"";
|
||||
position: fixed;
|
||||
inset: -20%;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px,
|
||||
transparent 1px, transparent 48px),
|
||||
repeating-linear-gradient(90deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px,
|
||||
transparent 1px, transparent 48px);
|
||||
transform: rotate(-7deg);
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
canvas {
|
||||
display:block;
|
||||
width:100vw;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import ClawdisKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
private final class ConnectStatusStore: ObservableObject {
|
||||
@Published var text: String?
|
||||
}
|
||||
|
||||
extension ConnectStatusStore: @unchecked Sendable {}
|
||||
|
||||
struct SettingsTab: View {
|
||||
@EnvironmentObject private var appModel: NodeAppModel
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@@ -8,7 +16,7 @@ struct SettingsTab: View {
|
||||
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
|
||||
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
|
||||
@StateObject private var discovery = BridgeDiscoveryModel()
|
||||
@State private var connectStatus: String?
|
||||
@StateObject private var connectStatus = ConnectStatusStore()
|
||||
@State private var connectingBridgeID: String?
|
||||
@State private var didAutoConnect = false
|
||||
|
||||
@@ -47,8 +55,8 @@ struct SettingsTab: View {
|
||||
self.bridgeList(showing: .all)
|
||||
}
|
||||
|
||||
if let connectStatus {
|
||||
Text(connectStatus)
|
||||
if let text = self.connectStatus.text {
|
||||
Text(text)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -77,22 +85,20 @@ struct SettingsTab: View {
|
||||
guard let existing, !existing.isEmpty else { return }
|
||||
guard let target = self.pickAutoConnectBridge(from: newValue) else { return }
|
||||
|
||||
self.didAutoConnect = true
|
||||
self.preferredBridgeStableID = target.stableID
|
||||
self.appModel.connectToBridge(
|
||||
endpoint: target.endpoint,
|
||||
hello: BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: existing,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion()
|
||||
)
|
||||
)
|
||||
self.connectStatus = nil
|
||||
}
|
||||
self.didAutoConnect = true
|
||||
self.preferredBridgeStableID = target.stableID
|
||||
self.appModel.connectToBridge(
|
||||
endpoint: target.endpoint,
|
||||
hello: BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: existing,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion()))
|
||||
self.connectStatus.text = nil
|
||||
}
|
||||
.onChange(of: self.appModel.bridgeServerName) { _, _ in
|
||||
self.connectStatus = nil
|
||||
self.connectStatus.text = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,22 +179,21 @@ struct SettingsTab: View {
|
||||
existing :
|
||||
nil
|
||||
|
||||
let hello = BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: existingToken,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion()
|
||||
)
|
||||
let token = try await BridgeClient().pairAndHello(
|
||||
endpoint: bridge.endpoint,
|
||||
hello: hello,
|
||||
onStatus: { status in
|
||||
Task { @MainActor in
|
||||
self.connectStatus = status
|
||||
}
|
||||
}
|
||||
)
|
||||
let hello = BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: existingToken,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion())
|
||||
let token = try await BridgeClient().pairAndHello(
|
||||
endpoint: bridge.endpoint,
|
||||
hello: hello,
|
||||
onStatus: { status in
|
||||
let store = self.connectStatus
|
||||
Task { @MainActor in
|
||||
store.text = status
|
||||
}
|
||||
})
|
||||
|
||||
if !token.isEmpty, token != existingToken {
|
||||
_ = KeychainStore.saveString(
|
||||
@@ -197,19 +202,17 @@ struct SettingsTab: View {
|
||||
account: self.keychainAccount())
|
||||
}
|
||||
|
||||
self.appModel.connectToBridge(
|
||||
endpoint: bridge.endpoint,
|
||||
hello: BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: token,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion()
|
||||
)
|
||||
)
|
||||
self.appModel.connectToBridge(
|
||||
endpoint: bridge.endpoint,
|
||||
hello: BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: token,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion()))
|
||||
|
||||
} catch {
|
||||
self.connectStatus = "Failed: \(error.localizedDescription)"
|
||||
self.connectStatus.text = "Failed: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,22 +87,22 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
return self.html("Forbidden", title: "Canvas: 403")
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: standardizedFile)
|
||||
let mime = CanvasScheme.mimeType(forExtension: standardizedFile.pathExtension)
|
||||
let servedPath = standardizedFile.path
|
||||
canvasLogger.debug(
|
||||
"served \(session, privacy: .public)/\(path, privacy: .public) -> \(servedPath, privacy: .public)")
|
||||
return CanvasResponse(mime: mime, data: data)
|
||||
} catch {
|
||||
let failedPath = standardizedFile.path
|
||||
let errorText = error.localizedDescription
|
||||
canvasLogger
|
||||
.error(
|
||||
"failed reading \(failedPath, privacy: .public): \(errorText, privacy: .public)")
|
||||
return self.html("Failed to read file.", title: "Canvas error")
|
||||
}
|
||||
}
|
||||
do {
|
||||
let data = try Data(contentsOf: standardizedFile)
|
||||
let mime = CanvasScheme.mimeType(forExtension: standardizedFile.pathExtension)
|
||||
let servedPath = standardizedFile.path
|
||||
canvasLogger.debug(
|
||||
"served \(session, privacy: .public)/\(path, privacy: .public) -> \(servedPath, privacy: .public)")
|
||||
return CanvasResponse(mime: mime, data: data)
|
||||
} catch {
|
||||
let failedPath = standardizedFile.path
|
||||
let errorText = error.localizedDescription
|
||||
canvasLogger
|
||||
.error(
|
||||
"failed reading \(failedPath, privacy: .public): \(errorText, privacy: .public)")
|
||||
return self.html("Failed to read file.", title: "Canvas error")
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveFileURL(sessionRoot: URL, requestPath: String) -> URL? {
|
||||
let fm = FileManager.default
|
||||
|
||||
@@ -4,6 +4,11 @@ import SwiftUI
|
||||
struct ConfigSettings: View {
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
private let labelColumnWidth: CGFloat = 120
|
||||
private static let browserAttachOnlyHelp =
|
||||
"When enabled, the browser server will only connect if the clawd browser is already running."
|
||||
private static let browserProfileNote =
|
||||
"Clawd uses a separate Chrome profile and ports (default 18791/18792) "
|
||||
+ "so it won’t interfere with your daily browser."
|
||||
@State private var configModel: String = ""
|
||||
@State private var customModel: String = ""
|
||||
@State private var configSaving = false
|
||||
@@ -203,16 +208,12 @@ struct ConfigSettings: View {
|
||||
.toggleStyle(.checkbox)
|
||||
.disabled(!self.browserEnabled)
|
||||
.onChange(of: self.browserAttachOnly) { _, _ in self.autosaveConfig() }
|
||||
.help(
|
||||
"When enabled, the browser server will only connect if the clawd browser is already running."
|
||||
)
|
||||
.help(Self.browserAttachOnlyHelp)
|
||||
}
|
||||
GridRow {
|
||||
Color.clear
|
||||
.frame(width: self.labelColumnWidth, height: 1)
|
||||
Text(
|
||||
"Clawd uses a separate Chrome profile and ports (default 18791/18792) so it won’t interfere with your daily browser."
|
||||
)
|
||||
Text(Self.browserProfileNote)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
@@ -144,15 +144,15 @@ final class ControlChannel: ObservableObject {
|
||||
}
|
||||
|
||||
// If the gateway explicitly rejects the hello (e.g., auth/token mismatch), surface it.
|
||||
if let urlErr = error as? URLError,
|
||||
urlErr.code == .dataNotAllowed // used for WS close 1008 auth failures
|
||||
{
|
||||
let reason = urlErr.failureURLString ?? urlErr.localizedDescription
|
||||
return
|
||||
"Gateway rejected token; set CLAWDIS_GATEWAY_TOKEN in the mac app environment " +
|
||||
"or clear it on the gateway. " +
|
||||
"Reason: \(reason)"
|
||||
}
|
||||
if let urlErr = error as? URLError,
|
||||
urlErr.code == .dataNotAllowed // used for WS close 1008 auth failures
|
||||
{
|
||||
let reason = urlErr.failureURLString ?? urlErr.localizedDescription
|
||||
return
|
||||
"Gateway rejected token; set CLAWDIS_GATEWAY_TOKEN in the mac app environment " +
|
||||
"or clear it on the gateway. " +
|
||||
"Reason: \(reason)"
|
||||
}
|
||||
|
||||
// Common misfire: we connected to localhost:18789 but the port is occupied
|
||||
// by some other process (e.g. a local dev gateway or a stuck SSH forward).
|
||||
|
||||
@@ -234,12 +234,12 @@ final actor ControlSocketServer {
|
||||
#if DEBUG
|
||||
// Debug-only escape hatch: allow unsigned/same-UID clients when explicitly opted in.
|
||||
// This keeps local development workable (e.g. a SwiftPM-built `clawdis-mac` binary).
|
||||
let env = ProcessInfo.processInfo.environment["CLAWDIS_ALLOW_UNSIGNED_SOCKET_CLIENTS"]
|
||||
if env == "1", let callerUID = self.uid(for: pid), callerUID == getuid() {
|
||||
self.logger.warning(
|
||||
"allowing unsigned same-UID socket client pid=\(pid) (CLAWDIS_ALLOW_UNSIGNED_SOCKET_CLIENTS=1)")
|
||||
return true
|
||||
}
|
||||
let env = ProcessInfo.processInfo.environment["CLAWDIS_ALLOW_UNSIGNED_SOCKET_CLIENTS"]
|
||||
if env == "1", let callerUID = self.uid(for: pid), callerUID == getuid() {
|
||||
self.logger.warning(
|
||||
"allowing unsigned same-UID socket client pid=\(pid) (CLAWDIS_ALLOW_UNSIGNED_SOCKET_CLIENTS=1)")
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
if let callerUID = self.uid(for: pid) {
|
||||
|
||||
@@ -69,13 +69,12 @@ struct CronSettings: View {
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
}
|
||||
Text(
|
||||
"Jobs are saved, but they will not run automatically until `cron.enabled` is set to `true` " +
|
||||
"and the Gateway restarts."
|
||||
)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Text(
|
||||
"Jobs are saved, but they will not run automatically until `cron.enabled` is set to `true` " +
|
||||
"and the Gateway restarts.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if let storePath = self.store.schedulerStorePath, !storePath.isEmpty {
|
||||
Text(storePath)
|
||||
.font(.caption.monospaced())
|
||||
@@ -497,6 +496,21 @@ private struct CronJobEditor: View {
|
||||
let onSave: ([String: Any]) -> Void
|
||||
|
||||
private let labelColumnWidth: CGFloat = 160
|
||||
private static let introText =
|
||||
"Create a schedule that wakes clawd via the Gateway. "
|
||||
+ "Use an isolated session for agent turns so your main chat stays clean."
|
||||
private static let sessionTargetNote =
|
||||
"Main jobs post a system event into the current main session. "
|
||||
+ "Isolated jobs run clawd in a dedicated session and can deliver results (WhatsApp/Telegram/etc)."
|
||||
private static let scheduleKindNote =
|
||||
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
|
||||
private static let isolatedPayloadNote =
|
||||
"Isolated jobs always run an agent turn. The result can be delivered to a surface, "
|
||||
+ "and a short summary is posted back to your main chat."
|
||||
private static let mainPayloadNote =
|
||||
"System events are injected into the current main session. Agent turns require an isolated session target."
|
||||
private static let mainSummaryNote =
|
||||
"Controls the label used when posting the completion summary back to the main session."
|
||||
|
||||
@State private var name: String = ""
|
||||
@State private var enabled: Bool = true
|
||||
@@ -527,9 +541,7 @@ private struct CronJobEditor: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(self.job == nil ? "New cron job" : "Edit cron job")
|
||||
.font(.title3.weight(.semibold))
|
||||
Text(
|
||||
"Create a schedule that wakes clawd via the Gateway. Use an isolated session for agent turns so your main chat stays clean."
|
||||
)
|
||||
Text(Self.introText)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -575,8 +587,7 @@ private struct CronJobEditor: View {
|
||||
Color.clear
|
||||
.frame(width: self.labelColumnWidth, height: 1)
|
||||
Text(
|
||||
"Main jobs post a system event into the current main session. Isolated jobs run clawd in a dedicated session and can deliver results (WhatsApp/Telegram/etc)."
|
||||
)
|
||||
Self.sessionTargetNote)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -601,8 +612,7 @@ private struct CronJobEditor: View {
|
||||
Color.clear
|
||||
.frame(width: self.labelColumnWidth, height: 1)
|
||||
Text(
|
||||
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
|
||||
)
|
||||
Self.scheduleKindNote)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -646,9 +656,7 @@ private struct CronJobEditor: View {
|
||||
GroupBox("Payload") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if self.sessionTarget == .isolated {
|
||||
Text(
|
||||
"Isolated jobs always run an agent turn. The result can be delivered to a surface, and a short summary is posted back to your main chat."
|
||||
)
|
||||
Text(Self.isolatedPayloadNote)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -669,8 +677,7 @@ private struct CronJobEditor: View {
|
||||
Color.clear
|
||||
.frame(width: self.labelColumnWidth, height: 1)
|
||||
Text(
|
||||
"System events are injected into the current main session. Agent turns require an isolated session target."
|
||||
)
|
||||
Self.mainPayloadNote)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -703,8 +710,7 @@ private struct CronJobEditor: View {
|
||||
Color.clear
|
||||
.frame(width: self.labelColumnWidth, height: 1)
|
||||
Text(
|
||||
"Controls the label used when posting the completion summary back to the main session."
|
||||
)
|
||||
Self.mainSummaryNote)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -914,14 +920,14 @@ private struct CronJobEditor: View {
|
||||
}()
|
||||
|
||||
if self.sessionTarget == .main, payload["kind"] as? String == "agentTurn" {
|
||||
throw NSError(
|
||||
domain: "Cron",
|
||||
code: 0,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey:
|
||||
"Main session jobs require systemEvent payloads (switch Session target to isolated).",
|
||||
])
|
||||
}
|
||||
throw NSError(
|
||||
domain: "Cron",
|
||||
code: 0,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey:
|
||||
"Main session jobs require systemEvent payloads (switch Session target to isolated).",
|
||||
])
|
||||
}
|
||||
|
||||
if self.sessionTarget == .isolated, payload["kind"] as? String == "systemEvent" {
|
||||
throw NSError(
|
||||
|
||||
@@ -141,17 +141,16 @@ struct DebugSettings: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Attach only")
|
||||
Toggle("", isOn: self.$attachExistingGatewayOnly)
|
||||
.labelsHidden()
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"When enabled in local mode, the mac app will only connect " +
|
||||
"to an already-running gateway " +
|
||||
"and will not start one itself."
|
||||
)
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Attach only")
|
||||
Toggle("", isOn: self.$attachExistingGatewayOnly)
|
||||
.labelsHidden()
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"When enabled in local mode, the mac app will only connect " +
|
||||
"to an already-running gateway " +
|
||||
"and will not start one itself.")
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Deep links")
|
||||
Toggle("", isOn: self.$deepLinkAgentEnabled)
|
||||
@@ -232,17 +231,16 @@ struct DebugSettings: View {
|
||||
|
||||
GridRow {
|
||||
self.gridLabel("Diagnostics")
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Toggle("Write rolling diagnostics log (JSONL)", isOn: self.$diagnosticsFileLogEnabled)
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"Writes a rotating, local-only diagnostics log under ~/Library/Logs/Clawdis/. " +
|
||||
"Enable only while actively debugging."
|
||||
)
|
||||
HStack(spacing: 8) {
|
||||
Button("Open folder") {
|
||||
NSWorkspace.shared.open(DiagnosticsFileLog.logDirectoryURL())
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Toggle("Write rolling diagnostics log (JSONL)", isOn: self.$diagnosticsFileLogEnabled)
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"Writes a rotating, local-only diagnostics log under ~/Library/Logs/Clawdis/. " +
|
||||
"Enable only while actively debugging.")
|
||||
HStack(spacing: 8) {
|
||||
Button("Open folder") {
|
||||
NSWorkspace.shared.open(DiagnosticsFileLog.logDirectoryURL())
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Button("Clear") {
|
||||
Task { try? await DiagnosticsFileLog.shared.clear() }
|
||||
@@ -485,13 +483,12 @@ struct DebugSettings: View {
|
||||
|
||||
private var canvasSection: some View {
|
||||
GroupBox("Canvas") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Toggle("Allow Canvas (agent)", isOn: self.$canvasEnabled)
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"When off, agent Canvas requests return “Canvas disabled by user”. " +
|
||||
"Manual debug actions still work."
|
||||
)
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Toggle("Allow Canvas (agent)", isOn: self.$canvasEnabled)
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"When off, agent Canvas requests return “Canvas disabled by user”. " +
|
||||
"Manual debug actions still work.")
|
||||
|
||||
HStack(spacing: 8) {
|
||||
TextField("Session", text: self.$canvasSessionKey)
|
||||
@@ -587,18 +584,17 @@ struct DebugSettings: View {
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: 280, alignment: .leading)
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Web chat")
|
||||
Toggle("Use SwiftUI web chat (glass)", isOn: self.$webChatSwiftUIEnabled)
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"When enabled, the menu bar chat window/panel uses the native SwiftUI UI instead of the " +
|
||||
"bundled WKWebView."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Web chat")
|
||||
Toggle("Use SwiftUI web chat (glass)", isOn: self.$webChatSwiftUIEnabled)
|
||||
.toggleStyle(.checkbox)
|
||||
.help(
|
||||
"When enabled, the menu bar chat window/panel uses the native SwiftUI UI instead of the " +
|
||||
"bundled WKWebView.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func runPortCheck() async {
|
||||
@@ -752,12 +748,12 @@ struct DebugSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func configURL() -> URL {
|
||||
FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".clawdis")
|
||||
.appendingPathComponent("clawdis.json")
|
||||
}
|
||||
}
|
||||
private func configURL() -> URL {
|
||||
FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".clawdis")
|
||||
.appendingPathComponent("clawdis.json")
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugSettings {
|
||||
// MARK: - Canvas debug actions
|
||||
@@ -854,8 +850,7 @@ extension DebugSettings {
|
||||
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let result = try await CanvasManager.shared.eval(
|
||||
sessionKey: session.isEmpty ? "main" : session,
|
||||
javaScript: self.canvasEvalJS
|
||||
)
|
||||
javaScript: self.canvasEvalJS)
|
||||
self.canvasEvalResult = result
|
||||
} catch {
|
||||
self.canvasError = error.localizedDescription
|
||||
@@ -870,8 +865,7 @@ extension DebugSettings {
|
||||
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let path = try await CanvasManager.shared.snapshot(
|
||||
sessionKey: session.isEmpty ? "main" : session,
|
||||
outPath: nil
|
||||
)
|
||||
outPath: nil)
|
||||
self.canvasSnapshotPath = path
|
||||
} catch {
|
||||
self.canvasError = error.localizedDescription
|
||||
@@ -879,22 +873,22 @@ extension DebugSettings {
|
||||
}
|
||||
}
|
||||
|
||||
private struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
configuration.label
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
configuration.content
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
private struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
configuration.label
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
configuration.content
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct DebugSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DebugSettings()
|
||||
#if DEBUG
|
||||
struct DebugSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DebugSettings()
|
||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,18 +125,18 @@ actor GatewayEndpointStore {
|
||||
for (_, continuation) in self.subscribers {
|
||||
continuation.yield(next)
|
||||
}
|
||||
switch next {
|
||||
case let .ready(mode, url, _):
|
||||
let modeDesc = String(describing: mode)
|
||||
let urlDesc = url.absoluteString
|
||||
self.logger
|
||||
.debug(
|
||||
"resolved endpoint mode=\(modeDesc, privacy: .public) url=\(urlDesc, privacy: .public)")
|
||||
case let .unavailable(mode, reason):
|
||||
let modeDesc = String(describing: mode)
|
||||
self.logger
|
||||
.debug(
|
||||
"endpoint unavailable mode=\(modeDesc, privacy: .public) reason=\(reason, privacy: .public)")
|
||||
}
|
||||
}
|
||||
switch next {
|
||||
case let .ready(mode, url, _):
|
||||
let modeDesc = String(describing: mode)
|
||||
let urlDesc = url.absoluteString
|
||||
self.logger
|
||||
.debug(
|
||||
"resolved endpoint mode=\(modeDesc, privacy: .public) url=\(urlDesc, privacy: .public)")
|
||||
case let .unavailable(mode, reason):
|
||||
let modeDesc = String(describing: mode)
|
||||
self.logger
|
||||
.debug(
|
||||
"endpoint unavailable mode=\(modeDesc, privacy: .public) reason=\(reason, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
struct GeneralSettings: View {
|
||||
@ObservedObject var state: AppState
|
||||
@ObservedObject private var healthStore = HealthStore.shared
|
||||
@ObservedObject private var gatewayManager = GatewayProcessManager.shared
|
||||
// swiftlint:disable:next inclusive_language
|
||||
@StateObject private var masterDiscovery = MasterDiscoveryModel()
|
||||
@State private var isInstallingCLI = false
|
||||
@State private var cliStatus: String?
|
||||
@State private var cliInstalled = false
|
||||
struct GeneralSettings: View {
|
||||
@ObservedObject var state: AppState
|
||||
@ObservedObject private var healthStore = HealthStore.shared
|
||||
@ObservedObject private var gatewayManager = GatewayProcessManager.shared
|
||||
// swiftlint:disable:next inclusive_language
|
||||
@StateObject private var masterDiscovery = MasterDiscoveryModel()
|
||||
@State private var isInstallingCLI = false
|
||||
@State private var cliStatus: String?
|
||||
@State private var cliInstalled = false
|
||||
@State private var cliInstallLocation: String?
|
||||
@State private var gatewayStatus: GatewayEnvironmentStatus = .checking
|
||||
@State private var gatewayInstallMessage: String?
|
||||
@@ -577,12 +577,12 @@ extension GeneralSettings {
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
// swiftlint:disable:next inclusive_language
|
||||
private func applyDiscoveredMaster(_ master: MasterDiscoveryModel.DiscoveredMaster) {
|
||||
let host = master.tailnetDns ?? master.lanHost
|
||||
guard let host else { return }
|
||||
let user = NSUserName()
|
||||
var target = "\(user)@\(host)"
|
||||
// swiftlint:disable:next inclusive_language
|
||||
private func applyDiscoveredMaster(_ master: MasterDiscoveryModel.DiscoveredMaster) {
|
||||
let host = master.tailnetDns ?? master.lanHost
|
||||
guard let host else { return }
|
||||
let user = NSUserName()
|
||||
var target = "\(user)@\(host)"
|
||||
if master.sshPort != 22 {
|
||||
target += ":\(master.sshPort)"
|
||||
}
|
||||
|
||||
@@ -45,16 +45,16 @@ struct OnboardingView: View {
|
||||
@State private var cliStatus: String?
|
||||
@State private var copied = false
|
||||
@State private var monitoringPermissions = false
|
||||
@State private var monitoringDiscovery = false
|
||||
@State private var cliInstalled = false
|
||||
@State private var cliInstallLocation: String?
|
||||
@State private var gatewayStatus: GatewayEnvironmentStatus = .checking
|
||||
@State private var gatewayInstalling = false
|
||||
@State private var gatewayInstallMessage: String?
|
||||
// swiftlint:disable:next inclusive_language
|
||||
@StateObject private var masterDiscovery = MasterDiscoveryModel()
|
||||
@ObservedObject private var state = AppStateStore.shared
|
||||
@ObservedObject private var permissionMonitor = PermissionMonitor.shared
|
||||
@State private var monitoringDiscovery = false
|
||||
@State private var cliInstalled = false
|
||||
@State private var cliInstallLocation: String?
|
||||
@State private var gatewayStatus: GatewayEnvironmentStatus = .checking
|
||||
@State private var gatewayInstalling = false
|
||||
@State private var gatewayInstallMessage: String?
|
||||
// swiftlint:disable:next inclusive_language
|
||||
@StateObject private var masterDiscovery = MasterDiscoveryModel()
|
||||
@ObservedObject private var state = AppStateStore.shared
|
||||
@ObservedObject private var permissionMonitor = PermissionMonitor.shared
|
||||
|
||||
private let pageWidth: CGFloat = 680
|
||||
private let contentHeight: CGFloat = 520
|
||||
@@ -116,17 +116,16 @@ struct OnboardingView: View {
|
||||
}
|
||||
|
||||
private func welcomePage() -> some View {
|
||||
self.onboardingPage {
|
||||
Text("Welcome to Clawdis")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
Text(
|
||||
"Your macOS menu bar companion for notifications, screenshots, and agent automation — " +
|
||||
"setup takes just a few minutes."
|
||||
)
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
self.onboardingPage {
|
||||
Text("Welcome to Clawdis")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
Text(
|
||||
"Your macOS menu bar companion for notifications, screenshots, and agent automation — " +
|
||||
"setup takes just a few minutes.")
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.frame(maxWidth: 560)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -141,16 +140,16 @@ struct OnboardingView: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Security notice")
|
||||
.font(.headline)
|
||||
Text(
|
||||
"""
|
||||
The connected AI agent (e.g. Claude) can trigger powerful actions on your Mac,
|
||||
including running
|
||||
commands, reading/writing files, and capturing screenshots — depending on the
|
||||
permissions you grant.
|
||||
Text(
|
||||
"""
|
||||
The connected AI agent (e.g. Claude) can trigger powerful actions on your Mac,
|
||||
including running
|
||||
commands, reading/writing files, and capturing screenshots — depending on the
|
||||
permissions you grant.
|
||||
|
||||
Only enable Clawdis if you understand the risks and trust the prompts
|
||||
and integrations you use.
|
||||
""")
|
||||
Only enable Clawdis if you understand the risks and trust the prompts
|
||||
and integrations you use.
|
||||
""")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -162,17 +161,16 @@ struct OnboardingView: View {
|
||||
}
|
||||
|
||||
private func connectionPage() -> some View {
|
||||
self.onboardingPage {
|
||||
Text("Where Clawdis runs")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
Text(
|
||||
"Clawdis has one primary Gateway (“master”) that runs continuously. " +
|
||||
"Connect locally or over SSH/Tailscale so the agent can work on any Mac."
|
||||
)
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
self.onboardingPage {
|
||||
Text("Where Clawdis runs")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
Text(
|
||||
"Clawdis has one primary Gateway (“master”) that runs continuously. " +
|
||||
"Connect locally or over SSH/Tailscale so the agent can work on any Mac.")
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.frame(maxWidth: 520)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -300,26 +298,25 @@ struct OnboardingView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
} else {
|
||||
Text(
|
||||
"Uses \"npm install -g clawdis@<version>\" on your PATH. " +
|
||||
"We keep the gateway on port 18789."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
} else {
|
||||
Text(
|
||||
"Uses \"npm install -g clawdis@<version>\" on your PATH. " +
|
||||
"We keep the gateway on port 18789.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next inclusive_language
|
||||
private func applyDiscoveredMaster(_ master: MasterDiscoveryModel.DiscoveredMaster) {
|
||||
let host = master.tailnetDns ?? master.lanHost
|
||||
guard let host else { return }
|
||||
let user = NSUserName()
|
||||
var target = "\(user)@\(host)"
|
||||
// swiftlint:disable:next inclusive_language
|
||||
private func applyDiscoveredMaster(_ master: MasterDiscoveryModel.DiscoveredMaster) {
|
||||
let host = master.tailnetDns ?? master.lanHost
|
||||
guard let host else { return }
|
||||
let user = NSUserName()
|
||||
var target = "\(user)@\(host)"
|
||||
if master.sshPort != 22 {
|
||||
target += ":\(master.sshPort)"
|
||||
}
|
||||
@@ -460,13 +457,13 @@ struct OnboardingView: View {
|
||||
|
||||
Text("Telegram")
|
||||
.font(.headline)
|
||||
self.featureRow(
|
||||
title: "Set `TELEGRAM_BOT_TOKEN`",
|
||||
subtitle: """
|
||||
Create a bot with @BotFather and set the token as an env var
|
||||
(or `telegram.botToken` in `~/.clawdis/clawdis.json`).
|
||||
""",
|
||||
systemImage: "key")
|
||||
self.featureRow(
|
||||
title: "Set `TELEGRAM_BOT_TOKEN`",
|
||||
subtitle: """
|
||||
Create a bot with @BotFather and set the token as an env var
|
||||
(or `telegram.botToken` in `~/.clawdis/clawdis.json`).
|
||||
""",
|
||||
systemImage: "key")
|
||||
self.featureRow(
|
||||
title: "Verify with `clawdis status --deep`",
|
||||
subtitle: "This probes both WhatsApp and the Telegram API and prints what’s configured.",
|
||||
@@ -491,11 +488,11 @@ struct OnboardingView: View {
|
||||
title: "Try Voice Wake",
|
||||
subtitle: "Enable Voice Wake in Settings for hands-free commands with a live transcript overlay.",
|
||||
systemImage: "waveform.circle")
|
||||
self.featureRow(
|
||||
title: "Use the panel + Canvas",
|
||||
subtitle: "Open the menu bar panel for quick chat; the agent can show previews " +
|
||||
"and richer visuals in Canvas.",
|
||||
systemImage: "rectangle.inset.filled.and.person.filled")
|
||||
self.featureRow(
|
||||
title: "Use the panel + Canvas",
|
||||
subtitle: "Open the menu bar panel for quick chat; the agent can show previews " +
|
||||
"and richer visuals in Canvas.",
|
||||
systemImage: "rectangle.inset.filled.and.person.filled")
|
||||
self.featureRow(
|
||||
title: "Test a notification",
|
||||
subtitle: "Send a quick notify via the menu bar to confirm sounds and permissions.",
|
||||
|
||||
@@ -9,109 +9,110 @@ import Speech
|
||||
import UserNotifications
|
||||
|
||||
enum PermissionManager {
|
||||
static func ensure(_ caps: [Capability], interactive: Bool) async -> [Capability: Bool] {
|
||||
var results: [Capability: Bool] = [:]
|
||||
for cap in caps {
|
||||
results[cap] = await self.ensureCapability(cap, interactive: interactive)
|
||||
}
|
||||
return results
|
||||
}
|
||||
static func ensure(_ caps: [Capability], interactive: Bool) async -> [Capability: Bool] {
|
||||
var results: [Capability: Bool] = [:]
|
||||
for cap in caps {
|
||||
results[cap] = await self.ensureCapability(cap, interactive: interactive)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
private static func ensureCapability(_ cap: Capability, interactive: Bool) async -> Bool {
|
||||
switch cap {
|
||||
case .notifications:
|
||||
return await self.ensureNotifications(interactive: interactive)
|
||||
case .appleScript:
|
||||
return await self.ensureAppleScript(interactive: interactive)
|
||||
case .accessibility:
|
||||
return await self.ensureAccessibility(interactive: interactive)
|
||||
case .screenRecording:
|
||||
return await self.ensureScreenRecording(interactive: interactive)
|
||||
case .microphone:
|
||||
return await self.ensureMicrophone(interactive: interactive)
|
||||
case .speechRecognition:
|
||||
return await self.ensureSpeechRecognition(interactive: interactive)
|
||||
}
|
||||
}
|
||||
private static func ensureCapability(_ cap: Capability, interactive: Bool) async -> Bool {
|
||||
switch cap {
|
||||
case .notifications:
|
||||
await self.ensureNotifications(interactive: interactive)
|
||||
case .appleScript:
|
||||
await self.ensureAppleScript(interactive: interactive)
|
||||
case .accessibility:
|
||||
await self.ensureAccessibility(interactive: interactive)
|
||||
case .screenRecording:
|
||||
await self.ensureScreenRecording(interactive: interactive)
|
||||
case .microphone:
|
||||
await self.ensureMicrophone(interactive: interactive)
|
||||
case .speechRecognition:
|
||||
await self.ensureSpeechRecognition(interactive: interactive)
|
||||
}
|
||||
}
|
||||
|
||||
private static func ensureNotifications(interactive: Bool) async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let settings = await center.notificationSettings()
|
||||
private static func ensureNotifications(interactive: Bool) async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let settings = await center.notificationSettings()
|
||||
|
||||
switch settings.authorizationStatus {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
return true
|
||||
case .notDetermined:
|
||||
guard interactive else { return false }
|
||||
let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
let updated = await center.notificationSettings()
|
||||
return granted && (updated.authorizationStatus == .authorized || updated.authorizationStatus == .provisional)
|
||||
case .denied:
|
||||
if interactive {
|
||||
NotificationPermissionHelper.openSettings()
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
switch settings.authorizationStatus {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
return true
|
||||
case .notDetermined:
|
||||
guard interactive else { return false }
|
||||
let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
let updated = await center.notificationSettings()
|
||||
return granted &&
|
||||
(updated.authorizationStatus == .authorized || updated.authorizationStatus == .provisional)
|
||||
case .denied:
|
||||
if interactive {
|
||||
NotificationPermissionHelper.openSettings()
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func ensureAppleScript(interactive: Bool) async -> Bool {
|
||||
let granted = await MainActor.run { AppleScriptPermission.isAuthorized() }
|
||||
if interactive, !granted {
|
||||
await AppleScriptPermission.requestAuthorization()
|
||||
}
|
||||
return await MainActor.run { AppleScriptPermission.isAuthorized() }
|
||||
}
|
||||
private static func ensureAppleScript(interactive: Bool) async -> Bool {
|
||||
let granted = await MainActor.run { AppleScriptPermission.isAuthorized() }
|
||||
if interactive, !granted {
|
||||
await AppleScriptPermission.requestAuthorization()
|
||||
}
|
||||
return await MainActor.run { AppleScriptPermission.isAuthorized() }
|
||||
}
|
||||
|
||||
private static func ensureAccessibility(interactive: Bool) async -> Bool {
|
||||
let trusted = await MainActor.run { AXIsProcessTrusted() }
|
||||
if interactive, !trusted {
|
||||
await MainActor.run {
|
||||
let opts: NSDictionary = ["AXTrustedCheckOptionPrompt": true]
|
||||
_ = AXIsProcessTrustedWithOptions(opts)
|
||||
}
|
||||
}
|
||||
return await MainActor.run { AXIsProcessTrusted() }
|
||||
}
|
||||
private static func ensureAccessibility(interactive: Bool) async -> Bool {
|
||||
let trusted = await MainActor.run { AXIsProcessTrusted() }
|
||||
if interactive, !trusted {
|
||||
await MainActor.run {
|
||||
let opts: NSDictionary = ["AXTrustedCheckOptionPrompt": true]
|
||||
_ = AXIsProcessTrustedWithOptions(opts)
|
||||
}
|
||||
}
|
||||
return await MainActor.run { AXIsProcessTrusted() }
|
||||
}
|
||||
|
||||
private static func ensureScreenRecording(interactive: Bool) async -> Bool {
|
||||
let granted = ScreenRecordingProbe.isAuthorized()
|
||||
if interactive, !granted {
|
||||
await ScreenRecordingProbe.requestAuthorization()
|
||||
}
|
||||
return ScreenRecordingProbe.isAuthorized()
|
||||
}
|
||||
private static func ensureScreenRecording(interactive: Bool) async -> Bool {
|
||||
let granted = ScreenRecordingProbe.isAuthorized()
|
||||
if interactive, !granted {
|
||||
await ScreenRecordingProbe.requestAuthorization()
|
||||
}
|
||||
return ScreenRecordingProbe.isAuthorized()
|
||||
}
|
||||
|
||||
private static func ensureMicrophone(interactive: Bool) async -> Bool {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||
switch status {
|
||||
case .authorized:
|
||||
return true
|
||||
case .notDetermined:
|
||||
guard interactive else { return false }
|
||||
return await AVCaptureDevice.requestAccess(for: .audio)
|
||||
case .denied, .restricted:
|
||||
if interactive {
|
||||
MicrophonePermissionHelper.openSettings()
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
private static func ensureMicrophone(interactive: Bool) async -> Bool {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||
switch status {
|
||||
case .authorized:
|
||||
return true
|
||||
case .notDetermined:
|
||||
guard interactive else { return false }
|
||||
return await AVCaptureDevice.requestAccess(for: .audio)
|
||||
case .denied, .restricted:
|
||||
if interactive {
|
||||
MicrophonePermissionHelper.openSettings()
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func ensureSpeechRecognition(interactive: Bool) async -> Bool {
|
||||
let status = SFSpeechRecognizer.authorizationStatus()
|
||||
if status == .notDetermined, interactive {
|
||||
await withUnsafeContinuation { (cont: UnsafeContinuation<Void, Never>) in
|
||||
SFSpeechRecognizer.requestAuthorization { _ in
|
||||
DispatchQueue.main.async { cont.resume() }
|
||||
}
|
||||
}
|
||||
}
|
||||
return SFSpeechRecognizer.authorizationStatus() == .authorized
|
||||
}
|
||||
private static func ensureSpeechRecognition(interactive: Bool) async -> Bool {
|
||||
let status = SFSpeechRecognizer.authorizationStatus()
|
||||
if status == .notDetermined, interactive {
|
||||
await withUnsafeContinuation { (cont: UnsafeContinuation<Void, Never>) in
|
||||
SFSpeechRecognizer.requestAuthorization { _ in
|
||||
DispatchQueue.main.async { cont.resume() }
|
||||
}
|
||||
}
|
||||
}
|
||||
return SFSpeechRecognizer.authorizationStatus() == .authorized
|
||||
}
|
||||
|
||||
static func voiceWakePermissionsGranted() -> Bool {
|
||||
let mic = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
|
||||
|
||||
@@ -451,8 +451,7 @@ struct WebChatView: View {
|
||||
Text(
|
||||
self.viewModel.healthOK
|
||||
? "This is the native SwiftUI debug chat."
|
||||
: "Connecting to the gateway…"
|
||||
)
|
||||
: "Connecting to the gateway…")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -105,8 +105,8 @@ enum BrowserCLI {
|
||||
sub: String,
|
||||
options: RunOptions,
|
||||
baseURL: URL,
|
||||
jsonOutput: Bool
|
||||
) async throws -> Int32 {
|
||||
jsonOutput: Bool) async throws -> Int32
|
||||
{
|
||||
switch sub {
|
||||
case "status":
|
||||
return try await self.handleStatus(baseURL: baseURL, jsonOutput: jsonOutput)
|
||||
@@ -172,8 +172,7 @@ enum BrowserCLI {
|
||||
method: "POST",
|
||||
url: url,
|
||||
body: ["url": urlString],
|
||||
timeoutInterval: 15.0
|
||||
)
|
||||
timeoutInterval: 15.0)
|
||||
self.printResult(jsonOutput: jsonOutput, res: res)
|
||||
return 0
|
||||
}
|
||||
@@ -188,8 +187,7 @@ enum BrowserCLI {
|
||||
method: "POST",
|
||||
url: url,
|
||||
body: ["targetId": id],
|
||||
timeoutInterval: 5.0
|
||||
)
|
||||
timeoutInterval: 5.0)
|
||||
self.printResult(jsonOutput: jsonOutput, res: res)
|
||||
return 0
|
||||
}
|
||||
@@ -250,8 +248,7 @@ enum BrowserCLI {
|
||||
"targetId": options.targetId ?? "",
|
||||
"await": options.awaitPromise,
|
||||
],
|
||||
timeoutInterval: 15.0
|
||||
)
|
||||
timeoutInterval: 15.0)
|
||||
|
||||
if jsonOutput {
|
||||
self.printJSON(ok: true, result: res)
|
||||
|
||||
@@ -58,276 +58,269 @@ struct ClawdisCLI {
|
||||
enum Kind {
|
||||
case generic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseCommandLine(args: [String]) throws -> ParsedCLIRequest {
|
||||
var args = args
|
||||
guard !args.isEmpty else { throw CLIError.help }
|
||||
let command = args.removeFirst()
|
||||
private static func parseCommandLine(args: [String]) throws -> ParsedCLIRequest {
|
||||
var args = args
|
||||
guard !args.isEmpty else { throw CLIError.help }
|
||||
let command = args.removeFirst()
|
||||
|
||||
switch command {
|
||||
case "--help", "-h", "help":
|
||||
throw CLIError.help
|
||||
switch command {
|
||||
case "--help", "-h", "help":
|
||||
throw CLIError.help
|
||||
|
||||
case "--version", "-V", "version":
|
||||
throw CLIError.version
|
||||
case "--version", "-V", "version":
|
||||
throw CLIError.version
|
||||
|
||||
case "notify":
|
||||
return try self.parseNotify(args: &args)
|
||||
case "notify":
|
||||
return try self.parseNotify(args: &args)
|
||||
|
||||
case "ensure-permissions":
|
||||
return self.parseEnsurePermissions(args: &args)
|
||||
case "ensure-permissions":
|
||||
return self.parseEnsurePermissions(args: &args)
|
||||
|
||||
case "run":
|
||||
return self.parseRunShell(args: &args)
|
||||
case "run":
|
||||
return self.parseRunShell(args: &args)
|
||||
|
||||
case "status":
|
||||
return ParsedCLIRequest(request: .status, kind: .generic)
|
||||
case "status":
|
||||
return ParsedCLIRequest(request: .status, kind: .generic)
|
||||
|
||||
case "rpc-status":
|
||||
return ParsedCLIRequest(request: .rpcStatus, kind: .generic)
|
||||
case "rpc-status":
|
||||
return ParsedCLIRequest(request: .rpcStatus, kind: .generic)
|
||||
|
||||
case "agent":
|
||||
return try self.parseAgent(args: &args)
|
||||
case "agent":
|
||||
return try self.parseAgent(args: &args)
|
||||
|
||||
case "node":
|
||||
return try self.parseNode(args: &args)
|
||||
case "node":
|
||||
return try self.parseNode(args: &args)
|
||||
|
||||
case "canvas":
|
||||
return try self.parseCanvas(args: &args)
|
||||
case "canvas":
|
||||
return try self.parseCanvas(args: &args)
|
||||
|
||||
default:
|
||||
throw CLIError.help
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw CLIError.help
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseNotify(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
var title: String?
|
||||
var body: String?
|
||||
var sound: String?
|
||||
var priority: NotificationPriority?
|
||||
var delivery: NotificationDelivery?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--title": title = args.popFirst()
|
||||
case "--body": body = args.popFirst()
|
||||
case "--sound": sound = args.popFirst()
|
||||
case "--priority":
|
||||
if let val = args.popFirst(), let p = NotificationPriority(rawValue: val) { priority = p }
|
||||
case "--delivery":
|
||||
if let val = args.popFirst(), let d = NotificationDelivery(rawValue: val) { delivery = d }
|
||||
default: break
|
||||
}
|
||||
}
|
||||
guard let t = title, let b = body else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .notify(title: t, body: b, sound: sound, priority: priority, delivery: delivery),
|
||||
kind: .generic
|
||||
)
|
||||
}
|
||||
private static func parseNotify(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
var title: String?
|
||||
var body: String?
|
||||
var sound: String?
|
||||
var priority: NotificationPriority?
|
||||
var delivery: NotificationDelivery?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--title": title = args.popFirst()
|
||||
case "--body": body = args.popFirst()
|
||||
case "--sound": sound = args.popFirst()
|
||||
case "--priority":
|
||||
if let val = args.popFirst(), let p = NotificationPriority(rawValue: val) { priority = p }
|
||||
case "--delivery":
|
||||
if let val = args.popFirst(), let d = NotificationDelivery(rawValue: val) { delivery = d }
|
||||
default: break
|
||||
}
|
||||
}
|
||||
guard let t = title, let b = body else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .notify(title: t, body: b, sound: sound, priority: priority, delivery: delivery),
|
||||
kind: .generic)
|
||||
}
|
||||
|
||||
private static func parseEnsurePermissions(args: inout [String]) -> ParsedCLIRequest {
|
||||
var caps: [Capability] = []
|
||||
var interactive = false
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--cap":
|
||||
if let val = args.popFirst(), let cap = Capability(rawValue: val) { caps.append(cap) }
|
||||
case "--interactive":
|
||||
interactive = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if caps.isEmpty { caps = Capability.allCases }
|
||||
return ParsedCLIRequest(request: .ensurePermissions(caps, interactive: interactive), kind: .generic)
|
||||
}
|
||||
private static func parseEnsurePermissions(args: inout [String]) -> ParsedCLIRequest {
|
||||
var caps: [Capability] = []
|
||||
var interactive = false
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--cap":
|
||||
if let val = args.popFirst(), let cap = Capability(rawValue: val) { caps.append(cap) }
|
||||
case "--interactive":
|
||||
interactive = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if caps.isEmpty { caps = Capability.allCases }
|
||||
return ParsedCLIRequest(request: .ensurePermissions(caps, interactive: interactive), kind: .generic)
|
||||
}
|
||||
|
||||
private static func parseRunShell(args: inout [String]) -> ParsedCLIRequest {
|
||||
var cwd: String?
|
||||
var env: [String: String] = [:]
|
||||
var timeout: Double?
|
||||
var needsSR = false
|
||||
var cmd: [String] = []
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--cwd":
|
||||
cwd = args.popFirst()
|
||||
case "--env":
|
||||
if let pair = args.popFirst() {
|
||||
self.parseEnvPair(pair, into: &env)
|
||||
}
|
||||
case "--timeout":
|
||||
if let val = args.popFirst(), let dbl = Double(val) { timeout = dbl }
|
||||
case "--needs-screen-recording":
|
||||
needsSR = true
|
||||
default:
|
||||
cmd.append(arg)
|
||||
}
|
||||
}
|
||||
return ParsedCLIRequest(
|
||||
request: .runShell(
|
||||
command: cmd,
|
||||
cwd: cwd,
|
||||
env: env.isEmpty ? nil : env,
|
||||
timeoutSec: timeout,
|
||||
needsScreenRecording: needsSR
|
||||
),
|
||||
kind: .generic
|
||||
)
|
||||
}
|
||||
private static func parseRunShell(args: inout [String]) -> ParsedCLIRequest {
|
||||
var cwd: String?
|
||||
var env: [String: String] = [:]
|
||||
var timeout: Double?
|
||||
var needsSR = false
|
||||
var cmd: [String] = []
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--cwd":
|
||||
cwd = args.popFirst()
|
||||
case "--env":
|
||||
if let pair = args.popFirst() {
|
||||
self.parseEnvPair(pair, into: &env)
|
||||
}
|
||||
case "--timeout":
|
||||
if let val = args.popFirst(), let dbl = Double(val) { timeout = dbl }
|
||||
case "--needs-screen-recording":
|
||||
needsSR = true
|
||||
default:
|
||||
cmd.append(arg)
|
||||
}
|
||||
}
|
||||
return ParsedCLIRequest(
|
||||
request: .runShell(
|
||||
command: cmd,
|
||||
cwd: cwd,
|
||||
env: env.isEmpty ? nil : env,
|
||||
timeoutSec: timeout,
|
||||
needsScreenRecording: needsSR),
|
||||
kind: .generic)
|
||||
}
|
||||
|
||||
private static func parseEnvPair(_ pair: String, into env: inout [String: String]) {
|
||||
guard let eq = pair.firstIndex(of: "=") else { return }
|
||||
let key = String(pair[..<eq])
|
||||
let value = String(pair[pair.index(after: eq)...])
|
||||
env[key] = value
|
||||
}
|
||||
private static func parseEnvPair(_ pair: String, into env: inout [String: String]) {
|
||||
guard let eq = pair.firstIndex(of: "=") else { return }
|
||||
let key = String(pair[..<eq])
|
||||
let value = String(pair[pair.index(after: eq)...])
|
||||
env[key] = value
|
||||
}
|
||||
|
||||
private static func parseAgent(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
var message: String?
|
||||
var thinking: String?
|
||||
var session: String?
|
||||
var deliver = false
|
||||
var to: String?
|
||||
private static func parseAgent(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
var message: String?
|
||||
var thinking: String?
|
||||
var session: String?
|
||||
var deliver = false
|
||||
var to: String?
|
||||
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--message": message = args.popFirst()
|
||||
case "--thinking": thinking = args.popFirst()
|
||||
case "--session": session = args.popFirst()
|
||||
case "--deliver": deliver = true
|
||||
case "--to": to = args.popFirst()
|
||||
default:
|
||||
if message == nil {
|
||||
message = arg
|
||||
}
|
||||
}
|
||||
}
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--message": message = args.popFirst()
|
||||
case "--thinking": thinking = args.popFirst()
|
||||
case "--session": session = args.popFirst()
|
||||
case "--deliver": deliver = true
|
||||
case "--to": to = args.popFirst()
|
||||
default:
|
||||
if message == nil {
|
||||
message = arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let message else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .agent(message: message, thinking: thinking, session: session, deliver: deliver, to: to),
|
||||
kind: .generic
|
||||
)
|
||||
}
|
||||
guard let message else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .agent(message: message, thinking: thinking, session: session, deliver: deliver, to: to),
|
||||
kind: .generic)
|
||||
}
|
||||
|
||||
private static func parseNode(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
guard let sub = args.popFirst() else { throw CLIError.help }
|
||||
switch sub {
|
||||
case "list":
|
||||
return ParsedCLIRequest(request: .nodeList, kind: .generic)
|
||||
case "invoke":
|
||||
var nodeId: String?
|
||||
var command: String?
|
||||
var paramsJSON: String?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--node": nodeId = args.popFirst()
|
||||
case "--command": command = args.popFirst()
|
||||
case "--params-json": paramsJSON = args.popFirst()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
guard let nodeId, let command else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON),
|
||||
kind: .generic
|
||||
)
|
||||
default:
|
||||
throw CLIError.help
|
||||
}
|
||||
}
|
||||
private static func parseNode(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
guard let sub = args.popFirst() else { throw CLIError.help }
|
||||
switch sub {
|
||||
case "list":
|
||||
return ParsedCLIRequest(request: .nodeList, kind: .generic)
|
||||
case "invoke":
|
||||
var nodeId: String?
|
||||
var command: String?
|
||||
var paramsJSON: String?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--node": nodeId = args.popFirst()
|
||||
case "--command": command = args.popFirst()
|
||||
case "--params-json": paramsJSON = args.popFirst()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
guard let nodeId, let command else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON),
|
||||
kind: .generic)
|
||||
default:
|
||||
throw CLIError.help
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseCanvas(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
guard let sub = args.popFirst() else { throw CLIError.help }
|
||||
switch sub {
|
||||
case "show":
|
||||
var session = "main"
|
||||
var path: String?
|
||||
let placement = self.parseCanvasPlacement(args: &args, session: &session, path: &path)
|
||||
return ParsedCLIRequest(
|
||||
request: .canvasShow(session: session, path: path, placement: placement),
|
||||
kind: .generic
|
||||
)
|
||||
case "hide":
|
||||
var session = "main"
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return ParsedCLIRequest(request: .canvasHide(session: session), kind: .generic)
|
||||
case "goto":
|
||||
var session = "main"
|
||||
var path: String?
|
||||
let placement = self.parseCanvasPlacement(args: &args, session: &session, path: &path)
|
||||
guard let path else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .canvasGoto(session: session, path: path, placement: placement),
|
||||
kind: .generic
|
||||
)
|
||||
case "eval":
|
||||
var session = "main"
|
||||
var js: String?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
case "--js": js = args.popFirst()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
guard let js else { throw CLIError.help }
|
||||
return ParsedCLIRequest(request: .canvasEval(session: session, javaScript: js), kind: .generic)
|
||||
case "snapshot":
|
||||
var session = "main"
|
||||
var outPath: String?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
case "--out": outPath = args.popFirst()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return ParsedCLIRequest(request: .canvasSnapshot(session: session, outPath: outPath), kind: .generic)
|
||||
default:
|
||||
throw CLIError.help
|
||||
}
|
||||
}
|
||||
private static func parseCanvas(args: inout [String]) throws -> ParsedCLIRequest {
|
||||
guard let sub = args.popFirst() else { throw CLIError.help }
|
||||
switch sub {
|
||||
case "show":
|
||||
var session = "main"
|
||||
var path: String?
|
||||
let placement = self.parseCanvasPlacement(args: &args, session: &session, path: &path)
|
||||
return ParsedCLIRequest(
|
||||
request: .canvasShow(session: session, path: path, placement: placement),
|
||||
kind: .generic)
|
||||
case "hide":
|
||||
var session = "main"
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return ParsedCLIRequest(request: .canvasHide(session: session), kind: .generic)
|
||||
case "goto":
|
||||
var session = "main"
|
||||
var path: String?
|
||||
let placement = self.parseCanvasPlacement(args: &args, session: &session, path: &path)
|
||||
guard let path else { throw CLIError.help }
|
||||
return ParsedCLIRequest(
|
||||
request: .canvasGoto(session: session, path: path, placement: placement),
|
||||
kind: .generic)
|
||||
case "eval":
|
||||
var session = "main"
|
||||
var js: String?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
case "--js": js = args.popFirst()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
guard let js else { throw CLIError.help }
|
||||
return ParsedCLIRequest(request: .canvasEval(session: session, javaScript: js), kind: .generic)
|
||||
case "snapshot":
|
||||
var session = "main"
|
||||
var outPath: String?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
case "--out": outPath = args.popFirst()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return ParsedCLIRequest(request: .canvasSnapshot(session: session, outPath: outPath), kind: .generic)
|
||||
default:
|
||||
throw CLIError.help
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseCanvasPlacement(
|
||||
args: inout [String],
|
||||
session: inout String,
|
||||
path: inout String?
|
||||
) -> CanvasPlacement? {
|
||||
var x: Double?
|
||||
var y: Double?
|
||||
var width: Double?
|
||||
var height: Double?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
case "--path": path = args.popFirst()
|
||||
case "--x": x = args.popFirst().flatMap(Double.init)
|
||||
case "--y": y = args.popFirst().flatMap(Double.init)
|
||||
case "--width": width = args.popFirst().flatMap(Double.init)
|
||||
case "--height": height = args.popFirst().flatMap(Double.init)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
if x == nil, y == nil, width == nil, height == nil { return nil }
|
||||
return CanvasPlacement(x: x, y: y, width: width, height: height)
|
||||
}
|
||||
private static func parseCanvasPlacement(
|
||||
args: inout [String],
|
||||
session: inout String,
|
||||
path: inout String?) -> CanvasPlacement?
|
||||
{
|
||||
var x: Double?
|
||||
var y: Double?
|
||||
var width: Double?
|
||||
var height: Double?
|
||||
while !args.isEmpty {
|
||||
let arg = args.removeFirst()
|
||||
switch arg {
|
||||
case "--session": session = args.popFirst() ?? session
|
||||
case "--path": path = args.popFirst()
|
||||
case "--x": x = args.popFirst().flatMap(Double.init)
|
||||
case "--y": y = args.popFirst().flatMap(Double.init)
|
||||
case "--width": width = args.popFirst().flatMap(Double.init)
|
||||
case "--height": height = args.popFirst().flatMap(Double.init)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
if x == nil, y == nil, width == nil, height == nil { return nil }
|
||||
return CanvasPlacement(x: x, y: y, width: width, height: height)
|
||||
}
|
||||
|
||||
private static func printText(parsed: ParsedCLIRequest, response: Response) throws {
|
||||
guard response.ok else {
|
||||
@@ -506,13 +499,13 @@ struct ClawdisCLI {
|
||||
_NSGetExecutablePath(ptr.baseAddress, &size)
|
||||
}
|
||||
guard result2 == 0 else { return nil }
|
||||
}
|
||||
}
|
||||
|
||||
let nulIndex = buffer.firstIndex(of: 0) ?? buffer.count
|
||||
let bytes = buffer.prefix(nulIndex).map { UInt8(bitPattern: $0) }
|
||||
guard let path = String(bytes: bytes, encoding: .utf8) else { return nil }
|
||||
return URL(fileURLWithPath: path).resolvingSymlinksInPath()
|
||||
}
|
||||
let nulIndex = buffer.firstIndex(of: 0) ?? buffer.count
|
||||
let bytes = buffer.prefix(nulIndex).map { UInt8(bitPattern: $0) }
|
||||
guard let path = String(bytes: bytes, encoding: .utf8) else { return nil }
|
||||
return URL(fileURLWithPath: path).resolvingSymlinksInPath()
|
||||
}
|
||||
|
||||
private static func loadPackageJSONVersion() -> String? {
|
||||
guard let exeURL = self.resolveExecutableURL() else { return nil }
|
||||
|
||||
@@ -323,17 +323,17 @@ enum UICLI {
|
||||
"screenshotPath": screenshotPath,
|
||||
"result": self.toJSONObject(detection),
|
||||
])
|
||||
} else {
|
||||
FileHandle.standardOutput.write(Data((screenshotPath + "\n").utf8))
|
||||
for el in detection.elements.all {
|
||||
let b = el.bounds
|
||||
let label = (el.label ?? el.value ?? "").replacingOccurrences(of: "\n", with: " ")
|
||||
let coords = "\(Int(b.origin.x)),\(Int(b.origin.y))"
|
||||
let size = "\(Int(b.size.width))x\(Int(b.size.height))"
|
||||
let line = "\(el.id)\t\(el.type)\t\(coords) \(size)\t\(label)\n"
|
||||
FileHandle.standardOutput.write(Data(line.utf8))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FileHandle.standardOutput.write(Data((screenshotPath + "\n").utf8))
|
||||
for el in detection.elements.all {
|
||||
let b = el.bounds
|
||||
let label = (el.label ?? el.value ?? "").replacingOccurrences(of: "\n", with: " ")
|
||||
let coords = "\(Int(b.origin.x)),\(Int(b.origin.y))"
|
||||
let size = "\(Int(b.size.width))x\(Int(b.size.height))"
|
||||
let line = "\(el.id)\t\(el.type)\t\(coords) \(size)\t\(label)\n"
|
||||
FileHandle.standardOutput.write(Data(line.utf8))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -522,16 +522,16 @@ enum UICLI {
|
||||
])
|
||||
}
|
||||
|
||||
do {
|
||||
return try await client.getMostRecentSnapshot(applicationBundleId: resolvedBundle)
|
||||
} catch {
|
||||
let command = "clawdis-mac ui see --bundle-id \(resolvedBundle)"
|
||||
let help = "No recent snapshot for \(resolvedBundle). Run `\(command)` first."
|
||||
throw NSError(domain: "clawdis.ui", code: 6, userInfo: [
|
||||
NSLocalizedDescriptionKey: help,
|
||||
])
|
||||
}
|
||||
}
|
||||
do {
|
||||
return try await client.getMostRecentSnapshot(applicationBundleId: resolvedBundle)
|
||||
} catch {
|
||||
let command = "clawdis-mac ui see --bundle-id \(resolvedBundle)"
|
||||
let help = "No recent snapshot for \(resolvedBundle). Run `\(command)` first."
|
||||
throw NSError(domain: "clawdis.ui", code: 6, userInfo: [
|
||||
NSLocalizedDescriptionKey: help,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - IO helpers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user