File size: 17,403 Bytes
4fc4790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
import CoreGraphics
import Foundation

// MARK: - Capabilities

public enum Capability: String, Codable, CaseIterable, Sendable {
    /// AppleScript / Automation access to control other apps (TCC Automation).
    case appleScript
    case notifications
    case accessibility
    case screenRecording
    case microphone
    case speechRecognition
    case camera
    case location
}

public enum CameraFacing: String, Codable, Sendable {
    case front
    case back
}

// MARK: - Requests

/// Notification interruption level (maps to UNNotificationInterruptionLevel)
public enum NotificationPriority: String, Codable, Sendable {
    case passive // silent, no wake
    case active // default
    case timeSensitive // breaks through Focus modes
}

/// Notification delivery mechanism.
public enum NotificationDelivery: String, Codable, Sendable {
    /// Use macOS notification center (UNUserNotificationCenter).
    case system
    /// Use an in-app overlay/toast (no Notification Center history).
    case overlay
    /// Prefer system; fall back to overlay when system isn't available.
    case auto
}

// MARK: - Canvas geometry

/// Optional placement hints for the Canvas panel.
/// Values are in screen coordinates (same as `NSWindow` frame).
public struct CanvasPlacement: Codable, Sendable {
    public var x: Double?
    public var y: Double?
    public var width: Double?
    public var height: Double?

    public init(x: Double? = nil, y: Double? = nil, width: Double? = nil, height: Double? = nil) {
        self.x = x
        self.y = y
        self.width = width
        self.height = height
    }
}

// MARK: - Canvas show result

public enum CanvasShowStatus: String, Codable, Sendable {
    /// Panel was shown, but no navigation occurred (no target passed and session already existed).
    case shown
    /// Target was a direct URL (http(s) or file).
    case web
    /// Local canvas target resolved to an existing file.
    case ok
    /// Local canvas target did not resolve to a file (404 page).
    case notFound
    /// Local scaffold fallback (e.g., no index.html present).
    case welcome
}

public struct CanvasShowResult: Codable, Sendable {
    /// Session directory on disk (e.g. `~/Library/Application Support/OpenClaw/canvas/<session>/`).
    public var directory: String
    /// Target as provided by the caller (may be nil/empty).
    public var target: String?
    /// Target actually navigated to (nil when no navigation occurred; defaults to "/" for a newly created session).
    public var effectiveTarget: String?
    public var status: CanvasShowStatus
    /// URL that was loaded (nil when no navigation occurred).
    public var url: String?

    public init(
        directory: String,
        target: String?,
        effectiveTarget: String?,
        status: CanvasShowStatus,
        url: String?)
    {
        self.directory = directory
        self.target = target
        self.effectiveTarget = effectiveTarget
        self.status = status
        self.url = url
    }
}

// MARK: - Canvas A2UI

public enum CanvasA2UICommand: String, Codable, Sendable {
    case pushJSONL
    case reset
}

public enum Request: Sendable {
    case notify(
        title: String,
        body: String,
        sound: String?,
        priority: NotificationPriority?,
        delivery: NotificationDelivery?)
    case ensurePermissions([Capability], interactive: Bool)
    case runShell(
        command: [String],
        cwd: String?,
        env: [String: String]?,
        timeoutSec: Double?,
        needsScreenRecording: Bool)
    case status
    case agent(message: String, thinking: String?, session: String?, deliver: Bool, to: String?)
    case rpcStatus
    case canvasPresent(session: String, path: String?, placement: CanvasPlacement?)
    case canvasHide(session: String)
    case canvasEval(session: String, javaScript: String)
    case canvasSnapshot(session: String, outPath: String?)
    case canvasA2UI(session: String, command: CanvasA2UICommand, jsonl: String?)
    case nodeList
    case nodeDescribe(nodeId: String)
    case nodeInvoke(nodeId: String, command: String, paramsJSON: String?)
    case cameraSnap(facing: CameraFacing?, maxWidth: Int?, quality: Double?, outPath: String?)
    case cameraClip(facing: CameraFacing?, durationMs: Int?, includeAudio: Bool, outPath: String?)
    case screenRecord(screenIndex: Int?, durationMs: Int?, fps: Double?, includeAudio: Bool, outPath: String?)
}

// MARK: - Responses

public struct Response: Codable, Sendable {
    public var ok: Bool
    public var message: String?
    /// Optional payload (PNG bytes, stdout text, etc.).
    public var payload: Data?

    public init(ok: Bool, message: String? = nil, payload: Data? = nil) {
        self.ok = ok
        self.message = message
        self.payload = payload
    }
}

// MARK: - Codable conformance for Request

extension Request: Codable {
    private enum CodingKeys: String, CodingKey {
        case type
        case title, body, sound, priority, delivery
        case caps, interactive
        case command, cwd, env, timeoutSec, needsScreenRecording
        case message, thinking, session, deliver, to
        case rpcStatus
        case path
        case javaScript
        case outPath
        case screenIndex
        case fps
        case canvasA2UICommand
        case jsonl
        case facing
        case maxWidth
        case quality
        case durationMs
        case includeAudio
        case placement
        case nodeId
        case nodeCommand
        case paramsJSON
    }

    private enum Kind: String, Codable {
        case notify
        case ensurePermissions
        case runShell
        case status
        case agent
        case rpcStatus
        case canvasPresent
        case canvasHide
        case canvasEval
        case canvasSnapshot
        case canvasA2UI
        case nodeList
        case nodeDescribe
        case nodeInvoke
        case cameraSnap
        case cameraClip
        case screenRecord
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .notify(title, body, sound, priority, delivery):
            try container.encode(Kind.notify, forKey: .type)
            try container.encode(title, forKey: .title)
            try container.encode(body, forKey: .body)
            try container.encodeIfPresent(sound, forKey: .sound)
            try container.encodeIfPresent(priority, forKey: .priority)
            try container.encodeIfPresent(delivery, forKey: .delivery)

        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 .runShell(command, cwd, env, timeoutSec, needsSR):
            try container.encode(Kind.runShell, forKey: .type)
            try container.encode(command, forKey: .command)
            try container.encodeIfPresent(cwd, forKey: .cwd)
            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)

        case let .agent(message, thinking, session, deliver, to):
            try container.encode(Kind.agent, forKey: .type)
            try container.encode(message, forKey: .message)
            try container.encodeIfPresent(thinking, forKey: .thinking)
            try container.encodeIfPresent(session, forKey: .session)
            try container.encode(deliver, forKey: .deliver)
            try container.encodeIfPresent(to, forKey: .to)

        case .rpcStatus:
            try container.encode(Kind.rpcStatus, forKey: .type)

        case let .canvasPresent(session, path, placement):
            try container.encode(Kind.canvasPresent, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encodeIfPresent(path, forKey: .path)
            try container.encodeIfPresent(placement, forKey: .placement)

        case let .canvasHide(session):
            try container.encode(Kind.canvasHide, forKey: .type)
            try container.encode(session, forKey: .session)

        case let .canvasEval(session, javaScript):
            try container.encode(Kind.canvasEval, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encode(javaScript, forKey: .javaScript)

        case let .canvasSnapshot(session, outPath):
            try container.encode(Kind.canvasSnapshot, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encodeIfPresent(outPath, forKey: .outPath)

        case let .canvasA2UI(session, command, jsonl):
            try container.encode(Kind.canvasA2UI, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encode(command, forKey: .canvasA2UICommand)
            try container.encodeIfPresent(jsonl, forKey: .jsonl)

        case .nodeList:
            try container.encode(Kind.nodeList, forKey: .type)

        case let .nodeDescribe(nodeId):
            try container.encode(Kind.nodeDescribe, forKey: .type)
            try container.encode(nodeId, forKey: .nodeId)

        case let .nodeInvoke(nodeId, command, paramsJSON):
            try container.encode(Kind.nodeInvoke, forKey: .type)
            try container.encode(nodeId, forKey: .nodeId)
            try container.encode(command, forKey: .nodeCommand)
            try container.encodeIfPresent(paramsJSON, forKey: .paramsJSON)

        case let .cameraSnap(facing, maxWidth, quality, outPath):
            try container.encode(Kind.cameraSnap, forKey: .type)
            try container.encodeIfPresent(facing, forKey: .facing)
            try container.encodeIfPresent(maxWidth, forKey: .maxWidth)
            try container.encodeIfPresent(quality, forKey: .quality)
            try container.encodeIfPresent(outPath, forKey: .outPath)

        case let .cameraClip(facing, durationMs, includeAudio, outPath):
            try container.encode(Kind.cameraClip, forKey: .type)
            try container.encodeIfPresent(facing, forKey: .facing)
            try container.encodeIfPresent(durationMs, forKey: .durationMs)
            try container.encode(includeAudio, forKey: .includeAudio)
            try container.encodeIfPresent(outPath, forKey: .outPath)

        case let .screenRecord(screenIndex, durationMs, fps, includeAudio, outPath):
            try container.encode(Kind.screenRecord, forKey: .type)
            try container.encodeIfPresent(screenIndex, forKey: .screenIndex)
            try container.encodeIfPresent(durationMs, forKey: .durationMs)
            try container.encodeIfPresent(fps, forKey: .fps)
            try container.encode(includeAudio, forKey: .includeAudio)
            try container.encodeIfPresent(outPath, forKey: .outPath)
        }
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let kind = try container.decode(Kind.self, forKey: .type)
        switch kind {
        case .notify:
            let title = try container.decode(String.self, forKey: .title)
            let body = try container.decode(String.self, forKey: .body)
            let sound = try container.decodeIfPresent(String.self, forKey: .sound)
            let priority = try container.decodeIfPresent(NotificationPriority.self, forKey: .priority)
            let delivery = try container.decodeIfPresent(NotificationDelivery.self, forKey: .delivery)
            self = .notify(title: title, body: body, sound: sound, priority: priority, delivery: delivery)

        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 .runShell:
            let command = try container.decode([String].self, forKey: .command)
            let cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
            let env = try container.decodeIfPresent([String: String].self, forKey: .env)
            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

        case .agent:
            let message = try container.decode(String.self, forKey: .message)
            let thinking = try container.decodeIfPresent(String.self, forKey: .thinking)
            let session = try container.decodeIfPresent(String.self, forKey: .session)
            let deliver = try container.decode(Bool.self, forKey: .deliver)
            let to = try container.decodeIfPresent(String.self, forKey: .to)
            self = .agent(message: message, thinking: thinking, session: session, deliver: deliver, to: to)

        case .rpcStatus:
            self = .rpcStatus

        case .canvasPresent:
            let session = try container.decode(String.self, forKey: .session)
            let path = try container.decodeIfPresent(String.self, forKey: .path)
            let placement = try container.decodeIfPresent(CanvasPlacement.self, forKey: .placement)
            self = .canvasPresent(session: session, path: path, placement: placement)

        case .canvasHide:
            let session = try container.decode(String.self, forKey: .session)
            self = .canvasHide(session: session)

        case .canvasEval:
            let session = try container.decode(String.self, forKey: .session)
            let javaScript = try container.decode(String.self, forKey: .javaScript)
            self = .canvasEval(session: session, javaScript: javaScript)

        case .canvasSnapshot:
            let session = try container.decode(String.self, forKey: .session)
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .canvasSnapshot(session: session, outPath: outPath)

        case .canvasA2UI:
            let session = try container.decode(String.self, forKey: .session)
            let command = try container.decode(CanvasA2UICommand.self, forKey: .canvasA2UICommand)
            let jsonl = try container.decodeIfPresent(String.self, forKey: .jsonl)
            self = .canvasA2UI(session: session, command: command, jsonl: jsonl)

        case .nodeList:
            self = .nodeList

        case .nodeDescribe:
            let nodeId = try container.decode(String.self, forKey: .nodeId)
            self = .nodeDescribe(nodeId: nodeId)

        case .nodeInvoke:
            let nodeId = try container.decode(String.self, forKey: .nodeId)
            let command = try container.decode(String.self, forKey: .nodeCommand)
            let paramsJSON = try container.decodeIfPresent(String.self, forKey: .paramsJSON)
            self = .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON)

        case .cameraSnap:
            let facing = try container.decodeIfPresent(CameraFacing.self, forKey: .facing)
            let maxWidth = try container.decodeIfPresent(Int.self, forKey: .maxWidth)
            let quality = try container.decodeIfPresent(Double.self, forKey: .quality)
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .cameraSnap(facing: facing, maxWidth: maxWidth, quality: quality, outPath: outPath)

        case .cameraClip:
            let facing = try container.decodeIfPresent(CameraFacing.self, forKey: .facing)
            let durationMs = try container.decodeIfPresent(Int.self, forKey: .durationMs)
            let includeAudio = (try? container.decode(Bool.self, forKey: .includeAudio)) ?? true
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .cameraClip(facing: facing, durationMs: durationMs, includeAudio: includeAudio, outPath: outPath)

        case .screenRecord:
            let screenIndex = try container.decodeIfPresent(Int.self, forKey: .screenIndex)
            let durationMs = try container.decodeIfPresent(Int.self, forKey: .durationMs)
            let fps = try container.decodeIfPresent(Double.self, forKey: .fps)
            let includeAudio = (try? container.decode(Bool.self, forKey: .includeAudio)) ?? true
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .screenRecord(
                screenIndex: screenIndex,
                durationMs: durationMs,
                fps: fps,
                includeAudio: includeAudio,
                outPath: outPath)
        }
    }
}

// Shared transport settings
public let controlSocketPath: String = {
    let home = FileManager().homeDirectoryForCurrentUser
    let preferred = home
        .appendingPathComponent("Library/Application Support/OpenClaw/control.sock")
        .path
    return preferred
}()