fix: use A2UI message context

This commit is contained in:
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 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?

View File

@@ -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 {

View File

@@ -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: " ")
}

View File

@@ -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`).
""",
])
}

View File

@@ -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: "Peters iPad",
instanceId: "ipad16,6",
session: .init(key: "main", surfaceId: "main"),
component: .init(id: "btnWeather", host: "Peters iPad", instanceId: "ipad16,6"),
contextJSON: "{\"city\":\"Vienna\"}")
let msg = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext)
#expect(msg.contains("CANVAS_A2UI "))
#expect(msg.contains("action=Get_Weather"))