fix: restore canvas action bridge
parent
78159a9435
commit
2b2f13ca79
|
|
@ -396,8 +396,7 @@ class NodeRuntime(context: Context) {
|
||||||
val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty {
|
val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty {
|
||||||
java.util.UUID.randomUUID().toString()
|
java.util.UUID.randomUUID().toString()
|
||||||
}
|
}
|
||||||
val name = (userActionObj["name"] as? JsonPrimitive)?.content?.trim().orEmpty()
|
val name = ClawdisCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch
|
||||||
if (name.isEmpty()) return@launch
|
|
||||||
|
|
||||||
val surfaceId =
|
val surfaceId =
|
||||||
(userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" }
|
(userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,24 @@
|
||||||
package com.steipete.clawdis.node.protocol
|
package com.steipete.clawdis.node.protocol
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
|
||||||
object ClawdisCanvasA2UIAction {
|
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 {
|
fun sanitizeTagValue(value: String): String {
|
||||||
val trimmed = value.trim().ifEmpty { "-" }
|
val trimmed = value.trim().ifEmpty { "-" }
|
||||||
val normalized = trimmed.replace(" ", "_")
|
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}\" } }));"
|
return "window.dispatchEvent(new CustomEvent('clawdis:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,11 +191,14 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
|
||||||
setBackgroundColor(Color.BLACK)
|
setBackgroundColor(Color.BLACK)
|
||||||
setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||||
|
|
||||||
addJavascriptInterface(
|
val a2uiBridge =
|
||||||
CanvasA2UIActionBridge { payload ->
|
CanvasA2UIActionBridge { payload ->
|
||||||
viewModel.handleCanvasA2UIActionFromWebView(payload)
|
viewModel.handleCanvasA2UIActionFromWebView(payload)
|
||||||
},
|
}
|
||||||
CanvasA2UIActionBridge.interfaceName,
|
addJavascriptInterface(a2uiBridge, CanvasA2UIActionBridge.interfaceName)
|
||||||
|
addJavascriptInterface(
|
||||||
|
CanvasA2UIActionLegacyBridge(a2uiBridge),
|
||||||
|
CanvasA2UIActionLegacyBridge.interfaceName,
|
||||||
)
|
)
|
||||||
viewModel.canvas.attach(this)
|
viewModel.canvas.attach(this)
|
||||||
}
|
}
|
||||||
|
|
@ -215,3 +218,19 @@ private class CanvasA2UIActionBridge(private val onMessage: (String) -> Unit) {
|
||||||
const val interfaceName: String = "clawdisCanvasA2UIAction"
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,24 @@
|
||||||
package com.steipete.clawdis.node.protocol
|
package com.steipete.clawdis.node.protocol
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ClawdisCanvasA2UIActionTest {
|
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
|
@Test
|
||||||
fun formatAgentMessageMatchesSharedSpec() {
|
fun formatAgentMessageMatchesSharedSpec() {
|
||||||
val msg =
|
val msg =
|
||||||
|
|
@ -32,4 +47,3 @@ class ClawdisCanvasA2UIActionTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@
|
||||||
<string>Clawdis can capture photos or short video clips when requested via the bridge.</string>
|
<string>Clawdis can capture photos or short video clips when requested via the bridge.</string>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>Clawdis discovers and connects to your Clawdis bridge on the local network.</string>
|
<string>Clawdis discovers and connects to your Clawdis bridge on the local network.</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Clawdis needs microphone access for voice wake.</string>
|
<string>Clawdis needs microphone access for voice wake.</string>
|
||||||
<key>NSSpeechRecognitionUsageDescription</key>
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ final class NodeAppModel {
|
||||||
}()
|
}()
|
||||||
guard !userAction.isEmpty else { return }
|
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 actionId: String = {
|
||||||
let id = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let id = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
return id.isEmpty ? UUID().uuidString : id
|
return id.isEmpty ? UUID().uuidString : id
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ final class ScreenController {
|
||||||
config.websiteDataStore = .nonPersistent()
|
config.websiteDataStore = .nonPersistent()
|
||||||
let a2uiActionHandler = CanvasA2UIActionMessageHandler()
|
let a2uiActionHandler = CanvasA2UIActionMessageHandler()
|
||||||
let userContentController = WKUserContentController()
|
let userContentController = WKUserContentController()
|
||||||
userContentController.add(a2uiActionHandler, name: CanvasA2UIActionMessageHandler.messageName)
|
for name in CanvasA2UIActionMessageHandler.handlerNames {
|
||||||
|
userContentController.add(a2uiActionHandler, name: name)
|
||||||
|
}
|
||||||
config.userContentController = userContentController
|
config.userContentController = userContentController
|
||||||
self.navigationDelegate = ScreenNavigationDelegate()
|
self.navigationDelegate = ScreenNavigationDelegate()
|
||||||
self.a2uiActionHandler = a2uiActionHandler
|
self.a2uiActionHandler = a2uiActionHandler
|
||||||
|
|
@ -323,6 +325,8 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
|
||||||
|
|
||||||
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||||
static let messageName = "clawdisCanvasA2UIAction"
|
static let messageName = "clawdisCanvasA2UIAction"
|
||||||
|
static let legacyMessageNames = ["canvas", "a2ui", "userAction", "action"]
|
||||||
|
static let handlerNames = [messageName] + legacyMessageNames
|
||||||
|
|
||||||
weak var controller: ScreenController?
|
weak var controller: ScreenController?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -639,7 +639,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
|
||||||
}()
|
}()
|
||||||
guard !userAction.isEmpty else { return }
|
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 =
|
let actionId =
|
||||||
(userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
(userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
||||||
?? UUID().uuidString
|
?? UUID().uuidString
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum ClawdisCanvasA2UIAction: Sendable {
|
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 {
|
public static func sanitizeTagValue(_ value: String) -> String {
|
||||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let nonEmpty = trimmed.isEmpty ? "-" : trimmed
|
let nonEmpty = trimmed.isEmpty ? "-" : trimmed
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ import Testing
|
||||||
#expect(ClawdisCanvasA2UIAction.sanitizeTagValue("macOS 26.2") == "macOS_26.2")
|
#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() {
|
@Test func formatAgentMessageIsTokenEfficientAndUnambiguous() {
|
||||||
let msg = ClawdisCanvasA2UIAction.formatAgentMessage(
|
let msg = ClawdisCanvasA2UIAction.formatAgentMessage(
|
||||||
actionName: "Get Weather",
|
actionName: "Get Weather",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue