diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index d15946c1b..5f1cf6d26 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -102,14 +102,12 @@ final class NodeAppModel { let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"]) let sessionKey = "main" - let message = ClawdisCanvasA2UIAction.formatAgentMessage( + let messageContext = ClawdisCanvasA2UIAction.AgentMessageContext( actionName: name, - sessionKey: sessionKey, - surfaceId: surfaceId, - sourceComponentId: sourceComponentId, - host: host, - instanceId: instanceId, + session: .init(key: sessionKey, surfaceId: surfaceId), + component: .init(id: sourceComponentId, host: host, instanceId: instanceId), contextJSON: contextJSON) + let message = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext) let ok: Bool var errorText: String? diff --git a/apps/macos/Sources/Clawdis/CanvasWindow.swift b/apps/macos/Sources/Clawdis/CanvasWindow.swift index eaa3b551b..d0fb3d344 100644 --- a/apps/macos/Sources/Clawdis/CanvasWindow.swift +++ b/apps/macos/Sources/Clawdis/CanvasWindow.swift @@ -654,14 +654,12 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"]) // Token-efficient and unambiguous. The agent should treat this as a UI event and (by default) update Canvas. - let text = ClawdisCanvasA2UIAction.formatAgentMessage( + let messageContext = ClawdisCanvasA2UIAction.AgentMessageContext( actionName: name, - sessionKey: self.sessionKey, - surfaceId: surfaceId, - sourceComponentId: sourceComponentId, - host: InstanceIdentity.displayName, - instanceId: instanceId, + session: .init(key: self.sessionKey, surfaceId: surfaceId), + component: .init(id: sourceComponentId, host: InstanceIdentity.displayName, instanceId: instanceId), contextJSON: contextJSON) + let text = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext) Task { [weak webView] in if AppStateStore.shared.connectionMode == .local { diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift index dc7a8fdaa..0acd06773 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift @@ -1,6 +1,42 @@ import Foundation public enum ClawdisCanvasA2UIAction: Sendable { + public struct AgentMessageContext: Sendable { + public struct Session: Sendable { + public var key: String + public var surfaceId: String + + public init(key: String, surfaceId: String) { + self.key = key + self.surfaceId = surfaceId + } + } + + public struct Component: Sendable { + public var id: String + public var host: String + public var instanceId: String + + public init(id: String, host: String, instanceId: String) { + self.id = id + self.host = host + self.instanceId = instanceId + } + } + + public var actionName: String + public var session: Session + public var component: Component + public var contextJSON: String? + + public init(actionName: String, session: Session, component: Component, contextJSON: String?) { + self.actionName = actionName + self.session = session + self.component = component + self.contextJSON = contextJSON + } + } + public static func extractActionName(_ userAction: [String: Any]) -> String? { let keys = ["name", "action"] for key in keys { @@ -30,25 +66,16 @@ public enum ClawdisCanvasA2UIAction: Sendable { return str } - public static func formatAgentMessage( - actionName: String, - sessionKey: String, - surfaceId: String, - sourceComponentId: String, - host: String, - instanceId: String, - contextJSON: String?) - -> String - { - let ctxSuffix = contextJSON.flatMap { $0.isEmpty ? nil : " ctx=\($0)" } ?? "" + public static func formatAgentMessage(_ context: AgentMessageContext) -> String { + let ctxSuffix = context.contextJSON.flatMap { $0.isEmpty ? nil : " ctx=\($0)" } ?? "" return [ "CANVAS_A2UI", - "action=\(self.sanitizeTagValue(actionName))", - "session=\(self.sanitizeTagValue(sessionKey))", - "surface=\(self.sanitizeTagValue(surfaceId))", - "component=\(self.sanitizeTagValue(sourceComponentId))", - "host=\(self.sanitizeTagValue(host))", - "instance=\(self.sanitizeTagValue(instanceId))\(ctxSuffix)", + "action=\(self.sanitizeTagValue(context.actionName))", + "session=\(self.sanitizeTagValue(context.session.key))", + "surface=\(self.sanitizeTagValue(context.session.surfaceId))", + "component=\(self.sanitizeTagValue(context.component.id))", + "host=\(self.sanitizeTagValue(context.component.host))", + "instance=\(self.sanitizeTagValue(context.component.instanceId))\(ctxSuffix)", "default=update_canvas", ].joined(separator: " ") } diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIJSONL.swift b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIJSONL.swift index 53ae33e62..0e8d3b4f5 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIJSONL.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIJSONL.swift @@ -27,7 +27,12 @@ public enum ClawdisCanvasA2UIJSONL: Sendable { } public static func validateV0_8(_ items: [ParsedItem]) throws { - let allowed = Set(["beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface"]) + let allowed = Set([ + "beginRendering", + "surfaceUpdate", + "dataModelUpdate", + "deleteSurface", + ]) for item in items { guard let dict = item.message.value as? [String: AnyCodable] else { throw NSError(domain: "A2UI", code: 1, userInfo: [ @@ -39,7 +44,8 @@ public enum ClawdisCanvasA2UIJSONL: Sendable { throw NSError(domain: "A2UI", code: 2, userInfo: [ NSLocalizedDescriptionKey: """ A2UI JSONL line \(item.lineNumber): looks like A2UI v0.9 (`createSurface`). - Canvas currently supports A2UI v0.8 server→client messages (`beginRendering`, `surfaceUpdate`, `dataModelUpdate`, `deleteSurface`). + Canvas currently supports A2UI v0.8 server→client messages + (`beginRendering`, `surfaceUpdate`, `dataModelUpdate`, `deleteSurface`). """, ]) } diff --git a/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift index 480dd4863..f2b2aa676 100644 --- a/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift +++ b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift @@ -17,14 +17,12 @@ import Testing } @Test func formatAgentMessageIsTokenEfficientAndUnambiguous() { - let msg = ClawdisCanvasA2UIAction.formatAgentMessage( + let messageContext = ClawdisCanvasA2UIAction.AgentMessageContext( actionName: "Get Weather", - sessionKey: "main", - surfaceId: "main", - sourceComponentId: "btnWeather", - host: "Peter’s iPad", - instanceId: "ipad16,6", + session: .init(key: "main", surfaceId: "main"), + component: .init(id: "btnWeather", host: "Peter’s iPad", instanceId: "ipad16,6"), contextJSON: "{\"city\":\"Vienna\"}") + let msg = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext) #expect(msg.contains("CANVAS_A2UI ")) #expect(msg.contains("action=Get_Weather"))