From 2b2f13ca793e8bb6f7119aea2f89fb1b3b4fb039 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 21 Dec 2025 00:53:11 +0100 Subject: [PATCH] fix: restore canvas action bridge --- .../com/steipete/clawdis/node/NodeRuntime.kt | 3 +-- .../node/protocol/ClawdisCanvasA2UIAction.kt | 19 +++++++++++++- .../steipete/clawdis/node/ui/RootScreen.kt | 25 ++++++++++++++++--- .../protocol/ClawdisCanvasA2UIActionTest.kt | 16 +++++++++++- apps/ios/Sources/Info.plist | 5 ++++ apps/ios/Sources/Model/NodeAppModel.swift | 2 +- .../ios/Sources/Screen/ScreenController.swift | 6 ++++- apps/macos/Sources/Clawdis/CanvasWindow.swift | 2 +- .../Sources/ClawdisKit/CanvasA2UIAction.swift | 11 ++++++++ .../CanvasA2UIActionTests.swift | 7 ++++++ 10 files changed, 86 insertions(+), 10 deletions(-) diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt index 5c79af817..d2e22a6c5 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt @@ -396,8 +396,7 @@ class NodeRuntime(context: Context) { val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { java.util.UUID.randomUUID().toString() } - val name = (userActionObj["name"] as? JsonPrimitive)?.content?.trim().orEmpty() - if (name.isEmpty()) return@launch + val name = ClawdisCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch val surfaceId = (userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" } diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIAction.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIAction.kt index 1245111e3..432a56fc8 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIAction.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIAction.kt @@ -1,6 +1,24 @@ package com.steipete.clawdis.node.protocol +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + object ClawdisCanvasA2UIAction { + fun extractActionName(userAction: JsonObject): String? { + val name = + (userAction["name"] as? JsonPrimitive) + ?.content + ?.trim() + .orEmpty() + if (name.isNotEmpty()) return name + val action = + (userAction["action"] as? JsonPrimitive) + ?.content + ?.trim() + .orEmpty() + return action.ifEmpty { null } + } + fun sanitizeTagValue(value: String): String { val trimmed = value.trim().ifEmpty { "-" } val normalized = trimmed.replace(" ", "_") @@ -46,4 +64,3 @@ object ClawdisCanvasA2UIAction { return "window.dispatchEvent(new CustomEvent('clawdis:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));" } } - diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt index 04f693813..38b61ed63 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt @@ -191,11 +191,14 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) setBackgroundColor(Color.BLACK) setLayerType(View.LAYER_TYPE_HARDWARE, null) - addJavascriptInterface( + val a2uiBridge = CanvasA2UIActionBridge { payload -> viewModel.handleCanvasA2UIActionFromWebView(payload) - }, - CanvasA2UIActionBridge.interfaceName, + } + addJavascriptInterface(a2uiBridge, CanvasA2UIActionBridge.interfaceName) + addJavascriptInterface( + CanvasA2UIActionLegacyBridge(a2uiBridge), + CanvasA2UIActionLegacyBridge.interfaceName, ) viewModel.canvas.attach(this) } @@ -215,3 +218,19 @@ private class CanvasA2UIActionBridge(private val onMessage: (String) -> Unit) { const val interfaceName: String = "clawdisCanvasA2UIAction" } } + +private class CanvasA2UIActionLegacyBridge(private val bridge: CanvasA2UIActionBridge) { + @JavascriptInterface + fun canvasAction(payload: String?) { + bridge.postMessage(payload) + } + + @JavascriptInterface + fun postMessage(payload: String?) { + bridge.postMessage(payload) + } + + companion object { + const val interfaceName: String = "Android" + } +} diff --git a/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIActionTest.kt b/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIActionTest.kt index 5bf5abfc3..237ccced6 100644 --- a/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIActionTest.kt +++ b/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisCanvasA2UIActionTest.kt @@ -1,9 +1,24 @@ package com.steipete.clawdis.node.protocol +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject import org.junit.Assert.assertEquals import org.junit.Test class ClawdisCanvasA2UIActionTest { + @Test + fun extractActionNameAcceptsNameOrAction() { + val nameObj = Json.parseToJsonElement("{\"name\":\"Hello\"}").jsonObject + assertEquals("Hello", ClawdisCanvasA2UIAction.extractActionName(nameObj)) + + val actionObj = Json.parseToJsonElement("{\"action\":\"Wave\"}").jsonObject + assertEquals("Wave", ClawdisCanvasA2UIAction.extractActionName(actionObj)) + + val fallbackObj = + Json.parseToJsonElement("{\"name\":\" \",\"action\":\"Fallback\"}").jsonObject + assertEquals("Fallback", ClawdisCanvasA2UIAction.extractActionName(fallbackObj)) + } + @Test fun formatAgentMessageMatchesSharedSpec() { val msg = @@ -32,4 +47,3 @@ class ClawdisCanvasA2UIActionTest { ) } } - diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index c2f22b5e6..b389837a6 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -30,6 +30,11 @@ Clawdis can capture photos or short video clips when requested via the bridge. NSLocalNetworkUsageDescription Clawdis discovers and connects to your Clawdis bridge on the local network. + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + NSMicrophoneUsageDescription Clawdis needs microphone access for voice wake. NSSpeechRecognitionUsageDescription diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index c01e1eca7..d15946c1b 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -80,7 +80,7 @@ final class NodeAppModel { }() guard !userAction.isEmpty else { return } - guard let name = userAction["name"] as? String, !name.isEmpty else { return } + guard let name = ClawdisCanvasA2UIAction.extractActionName(userAction) else { return } let actionId: String = { let id = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return id.isEmpty ? UUID().uuidString : id diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index e22ced7f0..4b0cca50c 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -24,7 +24,9 @@ final class ScreenController { config.websiteDataStore = .nonPersistent() let a2uiActionHandler = CanvasA2UIActionMessageHandler() let userContentController = WKUserContentController() - userContentController.add(a2uiActionHandler, name: CanvasA2UIActionMessageHandler.messageName) + for name in CanvasA2UIActionMessageHandler.handlerNames { + userContentController.add(a2uiActionHandler, name: name) + } config.userContentController = userContentController self.navigationDelegate = ScreenNavigationDelegate() self.a2uiActionHandler = a2uiActionHandler @@ -323,6 +325,8 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler { static let messageName = "clawdisCanvasA2UIAction" + static let legacyMessageNames = ["canvas", "a2ui", "userAction", "action"] + static let handlerNames = [messageName] + legacyMessageNames weak var controller: ScreenController? diff --git a/apps/macos/Sources/Clawdis/CanvasWindow.swift b/apps/macos/Sources/Clawdis/CanvasWindow.swift index 297bd6e1b..eaa3b551b 100644 --- a/apps/macos/Sources/Clawdis/CanvasWindow.swift +++ b/apps/macos/Sources/Clawdis/CanvasWindow.swift @@ -639,7 +639,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan }() guard !userAction.isEmpty else { return } - guard let name = userAction["name"] as? String, !name.isEmpty else { return } + guard let name = ClawdisCanvasA2UIAction.extractActionName(userAction) else { return } let actionId = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? UUID().uuidString diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift index f89f23d18..dc7a8fdaa 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift @@ -1,6 +1,17 @@ import Foundation public enum ClawdisCanvasA2UIAction: Sendable { + public static func extractActionName(_ userAction: [String: Any]) -> String? { + let keys = ["name", "action"] + for key in keys { + if let raw = userAction[key] as? String { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { return trimmed } + } + } + return nil + } + public static func sanitizeTagValue(_ value: String) -> String { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) let nonEmpty = trimmed.isEmpty ? "-" : trimmed diff --git a/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift index e69e821a2..480dd4863 100644 --- a/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift +++ b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/CanvasA2UIActionTests.swift @@ -9,6 +9,13 @@ import Testing #expect(ClawdisCanvasA2UIAction.sanitizeTagValue("macOS 26.2") == "macOS_26.2") } + @Test func extractActionNameAcceptsNameOrAction() { + #expect(ClawdisCanvasA2UIAction.extractActionName(["name": "Hello"]) == "Hello") + #expect(ClawdisCanvasA2UIAction.extractActionName(["action": "Wave"]) == "Wave") + #expect(ClawdisCanvasA2UIAction.extractActionName(["name": " ", "action": "Fallback"]) == "Fallback") + #expect(ClawdisCanvasA2UIAction.extractActionName(["action": " "]) == nil) + } + @Test func formatAgentMessageIsTokenEfficientAndUnambiguous() { let msg = ClawdisCanvasA2UIAction.formatAgentMessage( actionName: "Get Weather",