diff --git a/apps/ios/WatchApp/Sources/WatchInboxMessages.swift b/apps/ios/WatchApp/Sources/WatchInboxMessages.swift index bc207baa109a..38e1eee3afef 100644 --- a/apps/ios/WatchApp/Sources/WatchInboxMessages.swift +++ b/apps/ios/WatchApp/Sources/WatchInboxMessages.swift @@ -436,6 +436,42 @@ struct WatchAppSnapshotMessage: Codable, Equatable { var sentAtMs: Int64? var snapshotId: String? + private init( + gatewayStatus: OpenClawWatchAppStatus, + gatewayConnected: Bool, + agentName: String, + agentAvatarURL: String?, + agentAvatarText: String?, + sessionKey: String, + gatewayStableID: String?, + talkStatus: OpenClawWatchAppStatus, + talkEnabled: Bool, + talkListening: Bool, + talkSpeaking: Bool, + pendingApprovalCount: Int, + chatItems: [WatchChatItem]?, + chatStatus: OpenClawWatchAppStatus?, + sentAtMs: Int64?, + snapshotId: String?) + { + self.gatewayStatus = gatewayStatus + self.gatewayConnected = gatewayConnected + self.agentName = agentName + self.agentAvatarURL = agentAvatarURL + self.agentAvatarText = agentAvatarText + self.sessionKey = sessionKey + self.gatewayStableID = gatewayStableID + self.talkStatus = talkStatus + self.talkEnabled = talkEnabled + self.talkListening = talkListening + self.talkSpeaking = talkSpeaking + self.pendingApprovalCount = pendingApprovalCount + self.chatItems = chatItems + self.chatStatus = chatStatus + self.sentAtMs = sentAtMs + self.snapshotId = snapshotId + } + static func parsePayload(_ payload: [String: Any]) -> Self? { guard let type = payload["type"] as? String, type == WatchPayloadType.appSnapshot.rawValue @@ -548,21 +584,21 @@ struct WatchAppSnapshotMessage: Codable, Equatable { self.chatItems = try container.decodeIfPresent([WatchChatItem].self, forKey: .chatItems) self.sentAtMs = try container.decodeIfPresent(Int64.self, forKey: .sentAtMs) self.snapshotId = try container.decodeIfPresent(String.self, forKey: .snapshotId) - self.gatewayStatus = try container.decodeIfPresent( + self.gatewayStatus = (try? container.decode( OpenClawWatchAppStatus.self, - forKey: .gatewayStatus) ?? Self.decodeLegacyGatewayStatus( + forKey: .gatewayStatus)) ?? Self.decodeLegacyGatewayStatus( text: container.decodeIfPresent(String.self, forKey: .gatewayStatusText), connected: self.gatewayConnected) - self.talkStatus = try container.decodeIfPresent( + self.talkStatus = (try? container.decode( OpenClawWatchAppStatus.self, - forKey: .talkStatus) ?? Self.decodeLegacyTalkStatus( + forKey: .talkStatus)) ?? Self.decodeLegacyTalkStatus( text: container.decodeIfPresent(String.self, forKey: .talkStatusText), enabled: self.talkEnabled, listening: self.talkListening, speaking: self.talkSpeaking) - self.chatStatus = try container.decodeIfPresent( + self.chatStatus = (try? container.decode( OpenClawWatchAppStatus.self, - forKey: .chatStatus) ?? Self.decodeLegacyChatStatus( + forKey: .chatStatus)) ?? Self.decodeLegacyChatStatus( code: container.decodeIfPresent(String.self, forKey: .chatStatusCode), text: container.decodeIfPresent(String.self, forKey: .chatStatusText)) } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/WatchCommands.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/WatchCommands.swift index 3ffb5ede58b5..494a7074c5ed 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/WatchCommands.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/WatchCommands.swift @@ -384,6 +384,53 @@ public struct OpenClawWatchAppSnapshotMessage: Codable, Sendable, Equatable { self.snapshotId = snapshotId } + public init( + gatewayStatusText: String, + gatewayConnected: Bool, + agentName: String, + agentAvatarURL: String? = nil, + agentAvatarText: String? = nil, + sessionKey: String, + gatewayStableID: String? = nil, + talkStatusText: String, + talkEnabled: Bool, + talkListening: Bool, + talkSpeaking: Bool, + pendingApprovalCount: Int, + chatItems: [OpenClawWatchChatItem]? = nil, + chatStatusText: String? = nil, + sentAtMs: Int64? = nil, + snapshotId: String? = nil) + { + // Preserve the shipped source API while producers migrate to semantic statuses. + self.init( + gatewayStatus: Self.decodeLegacyGatewayStatus( + text: gatewayStatusText, + connected: gatewayConnected), + gatewayStatusText: gatewayStatusText, + gatewayConnected: gatewayConnected, + agentName: agentName, + agentAvatarURL: agentAvatarURL, + agentAvatarText: agentAvatarText, + sessionKey: sessionKey, + gatewayStableID: gatewayStableID, + talkStatus: Self.decodeLegacyTalkStatus( + text: talkStatusText, + enabled: talkEnabled, + listening: talkListening, + speaking: talkSpeaking), + talkStatusText: talkStatusText, + talkEnabled: talkEnabled, + talkListening: talkListening, + talkSpeaking: talkSpeaking, + pendingApprovalCount: pendingApprovalCount, + chatItems: chatItems, + chatStatus: Self.decodeLegacyChatStatus(code: nil, text: chatStatusText), + chatStatusText: chatStatusText, + sentAtMs: sentAtMs, + snapshotId: snapshotId) + } + private enum CodingKeys: String, CodingKey { case type case gatewayStatus @@ -426,25 +473,25 @@ public struct OpenClawWatchAppSnapshotMessage: Codable, Sendable, Equatable { self.snapshotId = try container.decodeIfPresent(String.self, forKey: .snapshotId) let gatewayStatusText = try container.decodeIfPresent(String.self, forKey: .gatewayStatusText) - self.gatewayStatus = try container.decodeIfPresent( + self.gatewayStatus = (try? container.decode( OpenClawWatchAppStatus.self, - forKey: .gatewayStatus) ?? Self.decodeLegacyGatewayStatus( + forKey: .gatewayStatus)) ?? Self.decodeLegacyGatewayStatus( text: gatewayStatusText, connected: self.gatewayConnected) self.gatewayStatusText = gatewayStatusText ?? Self.legacyText(for: self.gatewayStatus) let talkStatusText = try container.decodeIfPresent(String.self, forKey: .talkStatusText) - self.talkStatus = try container.decodeIfPresent( + self.talkStatus = (try? container.decode( OpenClawWatchAppStatus.self, - forKey: .talkStatus) ?? Self.decodeLegacyTalkStatus( + forKey: .talkStatus)) ?? Self.decodeLegacyTalkStatus( text: talkStatusText, enabled: self.talkEnabled, listening: self.talkListening, speaking: self.talkSpeaking) self.talkStatusText = talkStatusText ?? Self.legacyText(for: self.talkStatus) let chatStatusText = try container.decodeIfPresent(String.self, forKey: .chatStatusText) - self.chatStatus = try container.decodeIfPresent( + self.chatStatus = (try? container.decode( OpenClawWatchAppStatus.self, - forKey: .chatStatus) ?? Self.decodeLegacyChatStatus( + forKey: .chatStatus)) ?? Self.decodeLegacyChatStatus( code: container.decodeIfPresent(String.self, forKey: .chatStatusCode), text: chatStatusText) self.chatStatusText = chatStatusText ?? self.chatStatus.map(Self.legacyText) diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/WatchCommandsTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/WatchCommandsTests.swift index 5faf65a2a355..505565508bd5 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/WatchCommandsTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/WatchCommandsTests.swift @@ -29,4 +29,55 @@ struct WatchCommandsTests { #expect(object["chatStatus"] != nil) #expect(object["chatStatusText"] as? String == "No chat messages yet") } + + @Test func `shipped snapshot initializer remains available`() { + let message = OpenClawWatchAppSnapshotMessage( + gatewayStatusText: "Connected", + gatewayConnected: true, + agentName: "Main", + sessionKey: "main", + talkStatusText: "Off", + talkEnabled: false, + talkListening: false, + talkSpeaking: false, + pendingApprovalCount: 0) + + #expect(message.gatewayStatus.code == .gatewayConnected) + #expect(message.talkStatus.code == .talkOff) + } + + @Test func `unknown semantic statuses fall back to legacy text`() throws { + let json = """ + { + "type": "watch.appSnapshot", + "gatewayStatus": {"code": "futureGateway", "arguments": []}, + "gatewayStatusText": "Future gateway state", + "gatewayConnected": false, + "agentName": "Main", + "sessionKey": "main", + "talkStatus": {"code": "futureTalk", "arguments": []}, + "talkStatusText": "Future Talk state", + "talkEnabled": true, + "talkListening": false, + "talkSpeaking": false, + "pendingApprovalCount": 0, + "chatStatus": {"code": "futureChat", "arguments": []}, + "chatStatusText": "Future chat state" + } + """ + + let message = try JSONDecoder().decode( + OpenClawWatchAppSnapshotMessage.self, + from: Data(json.utf8)) + + #expect(message.gatewayStatus == OpenClawWatchAppStatus( + code: .legacy, + verbatim: "Future gateway state")) + #expect(message.talkStatus == OpenClawWatchAppStatus( + code: .legacy, + verbatim: "Future Talk state")) + #expect(message.chatStatus == OpenClawWatchAppStatus( + code: .legacy, + verbatim: "Future chat state")) + } }