From 33bf5cf42a3a79b0c2afffe96e30368250f4d838 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 18 Dec 2025 02:12:53 +0100 Subject: [PATCH] iOS: centralize canvas commands and capabilities --- .../Bridge/BridgeConnectionController.swift | 6 +-- apps/ios/Sources/Model/NodeAppModel.swift | 7 +--- apps/ios/Tests/CanvasCommandAliasTests.swift | 38 +++++++++++++++++++ .../Sources/ClawdisKit/CanvasCommands.swift | 28 ++++++++++++++ .../Sources/ClawdisKit/Capabilities.swift | 7 ++++ 5 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 apps/ios/Tests/CanvasCommandAliasTests.swift create mode 100644 apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift create mode 100644 apps/shared/ClawdisKit/Sources/ClawdisKit/Capabilities.swift diff --git a/apps/ios/Sources/Bridge/BridgeConnectionController.swift b/apps/ios/Sources/Bridge/BridgeConnectionController.swift index bb3b2443a..179098d0b 100644 --- a/apps/ios/Sources/Bridge/BridgeConnectionController.swift +++ b/apps/ios/Sources/Bridge/BridgeConnectionController.swift @@ -155,17 +155,17 @@ final class BridgeConnectionController { } private func currentCaps() -> [String] { - var caps = ["canvas"] + var caps = [ClawdisCapability.canvas.rawValue] // Default-on: if the key doesn't exist yet, treat it as enabled. let cameraEnabled = UserDefaults.standard.object(forKey: "camera.enabled") == nil ? true : UserDefaults.standard.bool(forKey: "camera.enabled") - if cameraEnabled { caps.append("camera") } + if cameraEnabled { caps.append(ClawdisCapability.camera.rawValue) } let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey) - if voiceWakeEnabled { caps.append("voiceWake") } + if voiceWakeEnabled { caps.append(ClawdisCapability.voiceWake.rawValue) } return caps } diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index c3b9a4974..01f76eadf 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -266,12 +266,7 @@ final class NodeAppModel { private func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse { // Alias for "canvas" capability: accept canvas.* commands and map them to screen.*. - let command = - if req.command.hasPrefix("canvas.") { - "screen." + req.command.dropFirst("canvas.".count) - } else { - req.command - } + let command = ClawdisInvokeCommandAliases.canonicalizeCanvasToScreen(req.command) if command.hasPrefix("screen.") || command.hasPrefix("camera."), self.isBackgrounded { return BridgeInvokeResponse( diff --git a/apps/ios/Tests/CanvasCommandAliasTests.swift b/apps/ios/Tests/CanvasCommandAliasTests.swift new file mode 100644 index 000000000..82f863fa7 --- /dev/null +++ b/apps/ios/Tests/CanvasCommandAliasTests.swift @@ -0,0 +1,38 @@ +import ClawdisKit +import Testing + +@Suite struct CanvasCommandAliasTests { + @Test func mapsKnownCanvasCommandsToScreen() { + let mappings: [(ClawdisCanvasCommand, ClawdisScreenCommand)] = [ + (.show, .show), + (.hide, .hide), + (.setMode, .setMode), + (.navigate, .navigate), + (.evalJS, .evalJS), + (.snapshot, .snapshot), + ] + + for (canvas, screen) in mappings { + #expect( + ClawdisInvokeCommandAliases.canonicalizeCanvasToScreen(canvas.rawValue) == + screen.rawValue) + } + } + + @Test func mapsUnknownCanvasNamespaceToScreen() { + #expect(ClawdisInvokeCommandAliases.canonicalizeCanvasToScreen("canvas.foo") == "screen.foo") + } + + @Test func leavesNonCanvasCommandsUnchanged() { + #expect( + ClawdisInvokeCommandAliases.canonicalizeCanvasToScreen(ClawdisCameraCommand.snap.rawValue) == + ClawdisCameraCommand.snap.rawValue) + } + + @Test func capabilitiesUseStableStrings() { + #expect(ClawdisCapability.canvas.rawValue == "canvas") + #expect(ClawdisCapability.camera.rawValue == "camera") + #expect(ClawdisCapability.voiceWake.rawValue == "voiceWake") + } +} + diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift new file mode 100644 index 000000000..08de9a118 --- /dev/null +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift @@ -0,0 +1,28 @@ +import Foundation + +public enum ClawdisCanvasCommand: String, Codable, Sendable { + case show = "canvas.show" + case hide = "canvas.hide" + case setMode = "canvas.setMode" + case navigate = "canvas.navigate" + case evalJS = "canvas.eval" + case snapshot = "canvas.snapshot" +} + +public enum ClawdisInvokeCommandAliases { + public static func canonicalizeCanvasToScreen(_ command: String) -> String { + if command.hasPrefix(ClawdisCanvasCommand.namespacePrefix) { + return ClawdisScreenCommand.namespacePrefix + + command.dropFirst(ClawdisCanvasCommand.namespacePrefix.count) + } + return command + } +} + +extension ClawdisCanvasCommand { + public static var namespacePrefix: String { "canvas." } +} + +extension ClawdisScreenCommand { + public static var namespacePrefix: String { "screen." } +} diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/Capabilities.swift b/apps/shared/ClawdisKit/Sources/ClawdisKit/Capabilities.swift new file mode 100644 index 000000000..25f98c301 --- /dev/null +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/Capabilities.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum ClawdisCapability: String, Codable, Sendable { + case canvas + case camera + case voiceWake +}