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",