fix: use A2UI message context

main
Peter Steinberger 2025-12-21 01:48:01 +01:00
parent fef1841fee
commit 406a94bf76
5 changed files with 64 additions and 37 deletions

View File

@ -102,14 +102,12 @@ final class NodeAppModel {
let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"]) let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"])
let sessionKey = "main" let sessionKey = "main"
let message = ClawdisCanvasA2UIAction.formatAgentMessage( let messageContext = ClawdisCanvasA2UIAction.AgentMessageContext(
actionName: name, actionName: name,
sessionKey: sessionKey, session: .init(key: sessionKey, surfaceId: surfaceId),
surfaceId: surfaceId, component: .init(id: sourceComponentId, host: host, instanceId: instanceId),
sourceComponentId: sourceComponentId,
host: host,
instanceId: instanceId,
contextJSON: contextJSON) contextJSON: contextJSON)
let message = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext)
let ok: Bool let ok: Bool
var errorText: String? var errorText: String?

View File

@ -654,14 +654,12 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"]) let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"])
// Token-efficient and unambiguous. The agent should treat this as a UI event and (by default) update Canvas. // 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, actionName: name,
sessionKey: self.sessionKey, session: .init(key: self.sessionKey, surfaceId: surfaceId),
surfaceId: surfaceId, component: .init(id: sourceComponentId, host: InstanceIdentity.displayName, instanceId: instanceId),
sourceComponentId: sourceComponentId,
host: InstanceIdentity.displayName,
instanceId: instanceId,
contextJSON: contextJSON) contextJSON: contextJSON)
let text = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext)
Task { [weak webView] in Task { [weak webView] in
if AppStateStore.shared.connectionMode == .local { if AppStateStore.shared.connectionMode == .local {

View File

@ -1,6 +1,42 @@
import Foundation import Foundation
public enum ClawdisCanvasA2UIAction: Sendable { 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? { public static func extractActionName(_ userAction: [String: Any]) -> String? {
let keys = ["name", "action"] let keys = ["name", "action"]
for key in keys { for key in keys {
@ -30,25 +66,16 @@ public enum ClawdisCanvasA2UIAction: Sendable {
return str return str
} }
public static func formatAgentMessage( public static func formatAgentMessage(_ context: AgentMessageContext) -> String {
actionName: String, let ctxSuffix = context.contextJSON.flatMap { $0.isEmpty ? nil : " ctx=\($0)" } ?? ""
sessionKey: String,
surfaceId: String,
sourceComponentId: String,
host: String,
instanceId: String,
contextJSON: String?)
-> String
{
let ctxSuffix = contextJSON.flatMap { $0.isEmpty ? nil : " ctx=\($0)" } ?? ""
return [ return [
"CANVAS_A2UI", "CANVAS_A2UI",
"action=\(self.sanitizeTagValue(actionName))", "action=\(self.sanitizeTagValue(context.actionName))",
"session=\(self.sanitizeTagValue(sessionKey))", "session=\(self.sanitizeTagValue(context.session.key))",
"surface=\(self.sanitizeTagValue(surfaceId))", "surface=\(self.sanitizeTagValue(context.session.surfaceId))",
"component=\(self.sanitizeTagValue(sourceComponentId))", "component=\(self.sanitizeTagValue(context.component.id))",
"host=\(self.sanitizeTagValue(host))", "host=\(self.sanitizeTagValue(context.component.host))",
"instance=\(self.sanitizeTagValue(instanceId))\(ctxSuffix)", "instance=\(self.sanitizeTagValue(context.component.instanceId))\(ctxSuffix)",
"default=update_canvas", "default=update_canvas",
].joined(separator: " ") ].joined(separator: " ")
} }

View File

@ -27,7 +27,12 @@ public enum ClawdisCanvasA2UIJSONL: Sendable {
} }
public static func validateV0_8(_ items: [ParsedItem]) throws { 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 { for item in items {
guard let dict = item.message.value as? [String: AnyCodable] else { guard let dict = item.message.value as? [String: AnyCodable] else {
throw NSError(domain: "A2UI", code: 1, userInfo: [ throw NSError(domain: "A2UI", code: 1, userInfo: [
@ -39,7 +44,8 @@ public enum ClawdisCanvasA2UIJSONL: Sendable {
throw NSError(domain: "A2UI", code: 2, userInfo: [ throw NSError(domain: "A2UI", code: 2, userInfo: [
NSLocalizedDescriptionKey: """ NSLocalizedDescriptionKey: """
A2UI JSONL line \(item.lineNumber): looks like A2UI v0.9 (`createSurface`). A2UI JSONL line \(item.lineNumber): looks like A2UI v0.9 (`createSurface`).
Canvas currently supports A2UI v0.8 serverclient messages (`beginRendering`, `surfaceUpdate`, `dataModelUpdate`, `deleteSurface`). Canvas currently supports A2UI v0.8 serverclient messages
(`beginRendering`, `surfaceUpdate`, `dataModelUpdate`, `deleteSurface`).
""", """,
]) ])
} }

View File

@ -17,14 +17,12 @@ import Testing
} }
@Test func formatAgentMessageIsTokenEfficientAndUnambiguous() { @Test func formatAgentMessageIsTokenEfficientAndUnambiguous() {
let msg = ClawdisCanvasA2UIAction.formatAgentMessage( let messageContext = ClawdisCanvasA2UIAction.AgentMessageContext(
actionName: "Get Weather", actionName: "Get Weather",
sessionKey: "main", session: .init(key: "main", surfaceId: "main"),
surfaceId: "main", component: .init(id: "btnWeather", host: "Peters iPad", instanceId: "ipad16,6"),
sourceComponentId: "btnWeather",
host: "Peters iPad",
instanceId: "ipad16,6",
contextJSON: "{\"city\":\"Vienna\"}") contextJSON: "{\"city\":\"Vienna\"}")
let msg = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext)
#expect(msg.contains("CANVAS_A2UI ")) #expect(msg.contains("CANVAS_A2UI "))
#expect(msg.contains("action=Get_Weather")) #expect(msg.contains("action=Get_Weather"))