mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-02-01 03:47:45 +01:00
Nodes: advertise canvas invoke commands
This commit is contained in:
@@ -266,6 +266,20 @@ class NodeRuntime(context: Context) {
|
|||||||
.joinToString(" ")
|
.joinToString(" ")
|
||||||
.trim()
|
.trim()
|
||||||
.ifEmpty { null }
|
.ifEmpty { null }
|
||||||
|
|
||||||
|
val invokeCommands =
|
||||||
|
buildList {
|
||||||
|
add("canvas.show")
|
||||||
|
add("canvas.hide")
|
||||||
|
add("canvas.setMode")
|
||||||
|
add("canvas.navigate")
|
||||||
|
add("canvas.eval")
|
||||||
|
add("canvas.snapshot")
|
||||||
|
if (cameraEnabled.value) {
|
||||||
|
add("camera.snap")
|
||||||
|
add("camera.clip")
|
||||||
|
}
|
||||||
|
}
|
||||||
val resolved =
|
val resolved =
|
||||||
if (storedToken.isNullOrBlank()) {
|
if (storedToken.isNullOrBlank()) {
|
||||||
_statusText.value = "Pairing…"
|
_statusText.value = "Pairing…"
|
||||||
@@ -288,6 +302,7 @@ class NodeRuntime(context: Context) {
|
|||||||
deviceFamily = "Android",
|
deviceFamily = "Android",
|
||||||
modelIdentifier = modelIdentifier,
|
modelIdentifier = modelIdentifier,
|
||||||
caps = caps,
|
caps = caps,
|
||||||
|
commands = invokeCommands,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -311,19 +326,20 @@ class NodeRuntime(context: Context) {
|
|||||||
platform = "Android",
|
platform = "Android",
|
||||||
version = "dev",
|
version = "dev",
|
||||||
deviceFamily = "Android",
|
deviceFamily = "Android",
|
||||||
modelIdentifier = modelIdentifier,
|
modelIdentifier = modelIdentifier,
|
||||||
caps =
|
caps =
|
||||||
buildList {
|
buildList {
|
||||||
add(ClawdisCapability.Canvas.rawValue)
|
add(ClawdisCapability.Canvas.rawValue)
|
||||||
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
|
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
|
||||||
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
|
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
|
||||||
add(ClawdisCapability.VoiceWake.rawValue)
|
add(ClawdisCapability.VoiceWake.rawValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
commands = invokeCommands,
|
||||||
)
|
),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun hasRecordAudioPermission(): Boolean {
|
private fun hasRecordAudioPermission(): Boolean {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class BridgePairingClient {
|
|||||||
val deviceFamily: String?,
|
val deviceFamily: String?,
|
||||||
val modelIdentifier: String?,
|
val modelIdentifier: String?,
|
||||||
val caps: List<String>?,
|
val caps: List<String>?,
|
||||||
|
val commands: List<String>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PairResult(val ok: Boolean, val token: String?, val error: String? = null)
|
data class PairResult(val ok: Boolean, val token: String?, val error: String? = null)
|
||||||
@@ -62,6 +63,7 @@ class BridgePairingClient {
|
|||||||
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
||||||
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
||||||
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
|
hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ class BridgePairingClient {
|
|||||||
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
||||||
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
||||||
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
|
hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class BridgeSession(
|
|||||||
val deviceFamily: String?,
|
val deviceFamily: String?,
|
||||||
val modelIdentifier: String?,
|
val modelIdentifier: String?,
|
||||||
val caps: List<String>?,
|
val caps: List<String>?,
|
||||||
|
val commands: List<String>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class InvokeRequest(val id: String, val command: String, val paramsJson: String?)
|
data class InvokeRequest(val id: String, val command: String, val paramsJson: String?)
|
||||||
@@ -198,6 +199,7 @@ class BridgeSession(
|
|||||||
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
||||||
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
||||||
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
|
hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class BridgePairingClientTest {
|
|||||||
deviceFamily = "Android",
|
deviceFamily = "Android",
|
||||||
modelIdentifier = "SM-X000",
|
modelIdentifier = "SM-X000",
|
||||||
caps = null,
|
caps = null,
|
||||||
|
commands = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertTrue(res.ok)
|
assertTrue(res.ok)
|
||||||
@@ -97,6 +98,7 @@ class BridgePairingClientTest {
|
|||||||
deviceFamily = "Android",
|
deviceFamily = "Android",
|
||||||
modelIdentifier = "SM-X000",
|
modelIdentifier = "SM-X000",
|
||||||
caps = null,
|
caps = null,
|
||||||
|
commands = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertTrue(res.ok)
|
assertTrue(res.ok)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class BridgeSessionTest {
|
|||||||
deviceFamily = null,
|
deviceFamily = null,
|
||||||
modelIdentifier = null,
|
modelIdentifier = null,
|
||||||
caps = null,
|
caps = null,
|
||||||
|
commands = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@ class BridgeSessionTest {
|
|||||||
deviceFamily = null,
|
deviceFamily = null,
|
||||||
modelIdentifier = null,
|
modelIdentifier = null,
|
||||||
caps = null,
|
caps = null,
|
||||||
|
commands = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
connected.await()
|
connected.await()
|
||||||
@@ -207,6 +209,7 @@ class BridgeSessionTest {
|
|||||||
deviceFamily = null,
|
deviceFamily = null,
|
||||||
modelIdentifier = null,
|
modelIdentifier = null,
|
||||||
caps = null,
|
caps = null,
|
||||||
|
commands = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
connected.await()
|
connected.await()
|
||||||
@@ -279,6 +282,7 @@ class BridgeSessionTest {
|
|||||||
deviceFamily = null,
|
deviceFamily = null,
|
||||||
modelIdentifier = null,
|
modelIdentifier = null,
|
||||||
caps = null,
|
caps = null,
|
||||||
|
commands = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -46,16 +46,17 @@ actor BridgeClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onStatus?("Requesting approval…")
|
onStatus?("Requesting approval…")
|
||||||
try await self.send(
|
try await self.send(
|
||||||
BridgePairRequest(
|
BridgePairRequest(
|
||||||
nodeId: hello.nodeId,
|
nodeId: hello.nodeId,
|
||||||
displayName: hello.displayName,
|
displayName: hello.displayName,
|
||||||
platform: hello.platform,
|
platform: hello.platform,
|
||||||
version: hello.version,
|
version: hello.version,
|
||||||
deviceFamily: hello.deviceFamily,
|
deviceFamily: hello.deviceFamily,
|
||||||
modelIdentifier: hello.modelIdentifier,
|
modelIdentifier: hello.modelIdentifier,
|
||||||
caps: hello.caps),
|
caps: hello.caps,
|
||||||
over: connection)
|
commands: hello.commands),
|
||||||
|
over: connection)
|
||||||
|
|
||||||
onStatus?("Waiting for approval…")
|
onStatus?("Waiting for approval…")
|
||||||
let ok = try await self.withTimeout(seconds: 60, purpose: "pairing approval") {
|
let ok = try await self.withTimeout(seconds: 60, purpose: "pairing approval") {
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ final class BridgeConnectionController {
|
|||||||
version: self.appVersion(),
|
version: self.appVersion(),
|
||||||
deviceFamily: self.deviceFamily(),
|
deviceFamily: self.deviceFamily(),
|
||||||
modelIdentifier: self.modelIdentifier(),
|
modelIdentifier: self.modelIdentifier(),
|
||||||
caps: self.currentCaps())
|
caps: self.currentCaps(),
|
||||||
|
commands: self.currentCommands())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolvedDisplayName(defaults: UserDefaults) -> String {
|
private func resolvedDisplayName(defaults: UserDefaults) -> String {
|
||||||
@@ -170,6 +171,25 @@ final class BridgeConnectionController {
|
|||||||
return caps
|
return caps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func currentCommands() -> [String] {
|
||||||
|
var commands: [String] = [
|
||||||
|
ClawdisCanvasCommand.show.rawValue,
|
||||||
|
ClawdisCanvasCommand.hide.rawValue,
|
||||||
|
ClawdisCanvasCommand.setMode.rawValue,
|
||||||
|
ClawdisCanvasCommand.navigate.rawValue,
|
||||||
|
ClawdisCanvasCommand.evalJS.rawValue,
|
||||||
|
ClawdisCanvasCommand.snapshot.rawValue,
|
||||||
|
]
|
||||||
|
|
||||||
|
let caps = Set(self.currentCaps())
|
||||||
|
if caps.contains(ClawdisCapability.camera.rawValue) {
|
||||||
|
commands.append(ClawdisCameraCommand.snap.rawValue)
|
||||||
|
commands.append(ClawdisCameraCommand.clip.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
private func platformString() -> String {
|
private func platformString() -> String {
|
||||||
let v = ProcessInfo.processInfo.operatingSystemVersion
|
let v = ProcessInfo.processInfo.operatingSystemVersion
|
||||||
let name = switch UIDevice.current.userInterfaceIdiom {
|
let name = switch UIDevice.current.userInterfaceIdiom {
|
||||||
|
|||||||
@@ -287,30 +287,30 @@ final class NodeAppModel {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
switch command {
|
switch command {
|
||||||
case ClawdisScreenCommand.show.rawValue:
|
case ClawdisCanvasCommand.show.rawValue:
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||||
|
|
||||||
case ClawdisScreenCommand.hide.rawValue:
|
case ClawdisCanvasCommand.hide.rawValue:
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||||
|
|
||||||
case ClawdisScreenCommand.setMode.rawValue:
|
case ClawdisCanvasCommand.setMode.rawValue:
|
||||||
let params = try Self.decodeParams(ClawdisScreenSetModeParams.self, from: req.paramsJSON)
|
let params = try Self.decodeParams(ClawdisCanvasSetModeParams.self, from: req.paramsJSON)
|
||||||
self.screen.setMode(params.mode)
|
self.screen.setMode(params.mode)
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||||
|
|
||||||
case ClawdisScreenCommand.navigate.rawValue:
|
case ClawdisCanvasCommand.navigate.rawValue:
|
||||||
let params = try Self.decodeParams(ClawdisScreenNavigateParams.self, from: req.paramsJSON)
|
let params = try Self.decodeParams(ClawdisCanvasNavigateParams.self, from: req.paramsJSON)
|
||||||
self.screen.navigate(to: params.url)
|
self.screen.navigate(to: params.url)
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||||
|
|
||||||
case ClawdisScreenCommand.evalJS.rawValue:
|
case ClawdisCanvasCommand.evalJS.rawValue:
|
||||||
let params = try Self.decodeParams(ClawdisScreenEvalParams.self, from: req.paramsJSON)
|
let params = try Self.decodeParams(ClawdisCanvasEvalParams.self, from: req.paramsJSON)
|
||||||
let result = try await self.screen.eval(javaScript: params.javaScript)
|
let result = try await self.screen.eval(javaScript: params.javaScript)
|
||||||
let payload = try Self.encodePayload(["result": result])
|
let payload = try Self.encodePayload(["result": result])
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||||
|
|
||||||
case ClawdisScreenCommand.snapshot.rawValue:
|
case ClawdisCanvasCommand.snapshot.rawValue:
|
||||||
let params = try? Self.decodeParams(ClawdisScreenSnapshotParams.self, from: req.paramsJSON)
|
let params = try? Self.decodeParams(ClawdisCanvasSnapshotParams.self, from: req.paramsJSON)
|
||||||
let maxWidth = params?.maxWidth.map { CGFloat($0) }
|
let maxWidth = params?.maxWidth.map { CGFloat($0) }
|
||||||
let base64 = try await self.screen.snapshotPNGBase64(maxWidth: maxWidth)
|
let base64 = try await self.screen.snapshotPNGBase64(maxWidth: maxWidth)
|
||||||
let payload = try Self.encodePayload(["format": "png", "base64": base64])
|
let payload = try Self.encodePayload(["format": "png", "base64": base64])
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ final class ScreenController {
|
|||||||
let webView: WKWebView
|
let webView: WKWebView
|
||||||
private let navigationDelegate: ScreenNavigationDelegate
|
private let navigationDelegate: ScreenNavigationDelegate
|
||||||
|
|
||||||
var mode: ClawdisScreenMode = .canvas
|
var mode: ClawdisCanvasMode = .canvas
|
||||||
var urlString: String = ""
|
var urlString: String = ""
|
||||||
var errorText: String?
|
var errorText: String?
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ final class ScreenController {
|
|||||||
self.reload()
|
self.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMode(_ mode: ClawdisScreenMode) {
|
func setMode(_ mode: ClawdisCanvasMode) {
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.reload()
|
self.reload()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,60 @@ struct SettingsTab: View {
|
|||||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
|
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func deviceFamily() -> String {
|
||||||
|
switch UIDevice.current.userInterfaceIdiom {
|
||||||
|
case .pad:
|
||||||
|
"iPad"
|
||||||
|
case .phone:
|
||||||
|
"iPhone"
|
||||||
|
default:
|
||||||
|
"iOS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func modelIdentifier() -> String {
|
||||||
|
var systemInfo = utsname()
|
||||||
|
uname(&systemInfo)
|
||||||
|
let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in
|
||||||
|
String(decoding: ptr.prefix { $0 != 0 }, as: UTF8.self)
|
||||||
|
}
|
||||||
|
return machine.isEmpty ? "unknown" : machine
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentCaps() -> [String] {
|
||||||
|
var caps = [ClawdisCapability.canvas.rawValue]
|
||||||
|
|
||||||
|
let cameraEnabled =
|
||||||
|
UserDefaults.standard.object(forKey: "camera.enabled") == nil
|
||||||
|
? true
|
||||||
|
: UserDefaults.standard.bool(forKey: "camera.enabled")
|
||||||
|
if cameraEnabled { caps.append(ClawdisCapability.camera.rawValue) }
|
||||||
|
|
||||||
|
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
|
||||||
|
if voiceWakeEnabled { caps.append(ClawdisCapability.voiceWake.rawValue) }
|
||||||
|
|
||||||
|
return caps
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentCommands() -> [String] {
|
||||||
|
var commands: [String] = [
|
||||||
|
ClawdisCanvasCommand.show.rawValue,
|
||||||
|
ClawdisCanvasCommand.hide.rawValue,
|
||||||
|
ClawdisCanvasCommand.setMode.rawValue,
|
||||||
|
ClawdisCanvasCommand.navigate.rawValue,
|
||||||
|
ClawdisCanvasCommand.evalJS.rawValue,
|
||||||
|
ClawdisCanvasCommand.snapshot.rawValue,
|
||||||
|
]
|
||||||
|
|
||||||
|
let caps = Set(self.currentCaps())
|
||||||
|
if caps.contains(ClawdisCapability.camera.rawValue) {
|
||||||
|
commands.append(ClawdisCameraCommand.snap.rawValue)
|
||||||
|
commands.append(ClawdisCameraCommand.clip.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
private func connect(_ bridge: BridgeDiscoveryModel.DiscoveredBridge) async {
|
private func connect(_ bridge: BridgeDiscoveryModel.DiscoveredBridge) async {
|
||||||
self.connectingBridgeID = bridge.id
|
self.connectingBridgeID = bridge.id
|
||||||
self.manualBridgeEnabled = false
|
self.manualBridgeEnabled = false
|
||||||
@@ -285,7 +339,11 @@ struct SettingsTab: View {
|
|||||||
displayName: self.displayName,
|
displayName: self.displayName,
|
||||||
token: existingToken,
|
token: existingToken,
|
||||||
platform: self.platformString(),
|
platform: self.platformString(),
|
||||||
version: self.appVersion())
|
version: self.appVersion(),
|
||||||
|
deviceFamily: self.deviceFamily(),
|
||||||
|
modelIdentifier: self.modelIdentifier(),
|
||||||
|
caps: self.currentCaps(),
|
||||||
|
commands: self.currentCommands())
|
||||||
let token = try await BridgeClient().pairAndHello(
|
let token = try await BridgeClient().pairAndHello(
|
||||||
endpoint: bridge.endpoint,
|
endpoint: bridge.endpoint,
|
||||||
hello: hello,
|
hello: hello,
|
||||||
@@ -309,7 +367,11 @@ struct SettingsTab: View {
|
|||||||
displayName: self.displayName,
|
displayName: self.displayName,
|
||||||
token: token,
|
token: token,
|
||||||
platform: self.platformString(),
|
platform: self.platformString(),
|
||||||
version: self.appVersion()))
|
version: self.appVersion(),
|
||||||
|
deviceFamily: self.deviceFamily(),
|
||||||
|
modelIdentifier: self.modelIdentifier(),
|
||||||
|
caps: self.currentCaps(),
|
||||||
|
commands: self.currentCommands()))
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
self.connectStatus.text = "Failed: \(error.localizedDescription)"
|
self.connectStatus.text = "Failed: \(error.localizedDescription)"
|
||||||
@@ -351,7 +413,11 @@ struct SettingsTab: View {
|
|||||||
displayName: self.displayName,
|
displayName: self.displayName,
|
||||||
token: existingToken,
|
token: existingToken,
|
||||||
platform: self.platformString(),
|
platform: self.platformString(),
|
||||||
version: self.appVersion())
|
version: self.appVersion(),
|
||||||
|
deviceFamily: self.deviceFamily(),
|
||||||
|
modelIdentifier: self.modelIdentifier(),
|
||||||
|
caps: self.currentCaps(),
|
||||||
|
commands: self.currentCommands())
|
||||||
let token = try await BridgeClient().pairAndHello(
|
let token = try await BridgeClient().pairAndHello(
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
hello: hello,
|
hello: hello,
|
||||||
@@ -375,7 +441,11 @@ struct SettingsTab: View {
|
|||||||
displayName: self.displayName,
|
displayName: self.displayName,
|
||||||
token: token,
|
token: token,
|
||||||
platform: self.platformString(),
|
platform: self.platformString(),
|
||||||
version: self.appVersion()))
|
version: self.appVersion(),
|
||||||
|
deviceFamily: self.deviceFamily(),
|
||||||
|
modelIdentifier: self.modelIdentifier(),
|
||||||
|
caps: self.currentCaps(),
|
||||||
|
commands: self.currentCommands()))
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
self.connectStatus.text = "Failed: \(error.localizedDescription)"
|
self.connectStatus.text = "Failed: \(error.localizedDescription)"
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ public struct BridgeHello: Codable, Sendable {
|
|||||||
public let deviceFamily: String?
|
public let deviceFamily: String?
|
||||||
public let modelIdentifier: String?
|
public let modelIdentifier: String?
|
||||||
public let caps: [String]?
|
public let caps: [String]?
|
||||||
|
public let commands: [String]?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
type: String = "hello",
|
type: String = "hello",
|
||||||
@@ -76,7 +77,8 @@ public struct BridgeHello: Codable, Sendable {
|
|||||||
version: String?,
|
version: String?,
|
||||||
deviceFamily: String? = nil,
|
deviceFamily: String? = nil,
|
||||||
modelIdentifier: String? = nil,
|
modelIdentifier: String? = nil,
|
||||||
caps: [String]? = nil)
|
caps: [String]? = nil,
|
||||||
|
commands: [String]? = nil)
|
||||||
{
|
{
|
||||||
self.type = type
|
self.type = type
|
||||||
self.nodeId = nodeId
|
self.nodeId = nodeId
|
||||||
@@ -87,6 +89,7 @@ public struct BridgeHello: Codable, Sendable {
|
|||||||
self.deviceFamily = deviceFamily
|
self.deviceFamily = deviceFamily
|
||||||
self.modelIdentifier = modelIdentifier
|
self.modelIdentifier = modelIdentifier
|
||||||
self.caps = caps
|
self.caps = caps
|
||||||
|
self.commands = commands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +112,7 @@ public struct BridgePairRequest: Codable, Sendable {
|
|||||||
public let deviceFamily: String?
|
public let deviceFamily: String?
|
||||||
public let modelIdentifier: String?
|
public let modelIdentifier: String?
|
||||||
public let caps: [String]?
|
public let caps: [String]?
|
||||||
|
public let commands: [String]?
|
||||||
public let remoteAddress: String?
|
public let remoteAddress: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -120,6 +124,7 @@ public struct BridgePairRequest: Codable, Sendable {
|
|||||||
deviceFamily: String? = nil,
|
deviceFamily: String? = nil,
|
||||||
modelIdentifier: String? = nil,
|
modelIdentifier: String? = nil,
|
||||||
caps: [String]? = nil,
|
caps: [String]? = nil,
|
||||||
|
commands: [String]? = nil,
|
||||||
remoteAddress: String? = nil)
|
remoteAddress: String? = nil)
|
||||||
{
|
{
|
||||||
self.type = type
|
self.type = type
|
||||||
@@ -130,6 +135,7 @@ public struct BridgePairRequest: Codable, Sendable {
|
|||||||
self.deviceFamily = deviceFamily
|
self.deviceFamily = deviceFamily
|
||||||
self.modelIdentifier = modelIdentifier
|
self.modelIdentifier = modelIdentifier
|
||||||
self.caps = caps
|
self.caps = caps
|
||||||
|
self.commands = commands
|
||||||
self.remoteAddress = remoteAddress
|
self.remoteAddress = remoteAddress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum ClawdisCanvasMode: String, Codable, Sendable {
|
||||||
|
case canvas
|
||||||
|
case web
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ClawdisCanvasNavigateParams: Codable, Sendable, Equatable {
|
||||||
|
public var url: String
|
||||||
|
|
||||||
|
public init(url: String) {
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ClawdisCanvasSetModeParams: Codable, Sendable, Equatable {
|
||||||
|
public var mode: ClawdisCanvasMode
|
||||||
|
|
||||||
|
public init(mode: ClawdisCanvasMode) {
|
||||||
|
self.mode = mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ClawdisCanvasEvalParams: Codable, Sendable, Equatable {
|
||||||
|
public var javaScript: String
|
||||||
|
|
||||||
|
public init(javaScript: String) {
|
||||||
|
self.javaScript = javaScript
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ClawdisCanvasSnapshotFormat: String, Codable, Sendable {
|
||||||
|
case png
|
||||||
|
case jpeg
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ClawdisCanvasSnapshotParams: Codable, Sendable, Equatable {
|
||||||
|
public var maxWidth: Int?
|
||||||
|
public var quality: Double?
|
||||||
|
public var format: ClawdisCanvasSnapshotFormat?
|
||||||
|
|
||||||
|
public init(maxWidth: Int? = nil, quality: Double? = nil, format: ClawdisCanvasSnapshotFormat? = nil) {
|
||||||
|
self.maxWidth = maxWidth
|
||||||
|
self.quality = quality
|
||||||
|
self.format = format
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,21 +8,3 @@ public enum ClawdisCanvasCommand: String, Codable, Sendable {
|
|||||||
case evalJS = "canvas.eval"
|
case evalJS = "canvas.eval"
|
||||||
case snapshot = "canvas.snapshot"
|
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." }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
public enum ClawdisScreenMode: String, Codable, Sendable {
|
|
||||||
case canvas
|
|
||||||
case web
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ClawdisScreenCommand: 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 struct ClawdisScreenNavigateParams: Codable, Sendable, Equatable {
|
|
||||||
public var url: String
|
|
||||||
|
|
||||||
public init(url: String) {
|
|
||||||
self.url = url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ClawdisScreenSetModeParams: Codable, Sendable, Equatable {
|
|
||||||
public var mode: ClawdisScreenMode
|
|
||||||
|
|
||||||
public init(mode: ClawdisScreenMode) {
|
|
||||||
self.mode = mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ClawdisScreenEvalParams: Codable, Sendable, Equatable {
|
|
||||||
public var javaScript: String
|
|
||||||
|
|
||||||
public init(javaScript: String) {
|
|
||||||
self.javaScript = javaScript
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ClawdisSnapshotFormat: String, Codable, Sendable {
|
|
||||||
case png
|
|
||||||
case jpeg
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ClawdisScreenSnapshotParams: Codable, Sendable, Equatable {
|
|
||||||
public var maxWidth: Int?
|
|
||||||
public var quality: Double?
|
|
||||||
public var format: ClawdisSnapshotFormat?
|
|
||||||
|
|
||||||
public init(maxWidth: Int? = nil, quality: Double? = nil, format: ClawdisSnapshotFormat? = nil) {
|
|
||||||
self.maxWidth = maxWidth
|
|
||||||
self.quality = quality
|
|
||||||
self.format = format
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user